Files
df7832aa43 Replaced our NPC AI with Behavior Trees. (#96628)
This PR replaces our current NPC AI with a [behavior tree
system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)).
Behavior trees are a common way of creating AI in which you place nodes
in a tree structure to define what actions an AI should take.

AI controllers defined a list of /datum/ai_planning_subtree types in
behavior_nodes. Each subtree was a self-contained unit that could call
queue_behavior() to fire off /datum/ai_behavior actions. The controller
iterated subtrees in order, each one deciding independently whether to
queue something and deciding whether the next subtree would run.

This has a few issues:
1. There's no real structure; you are just defining a list of things to
try in order.
2. There was a loooot of subtrees that were basically the same as
another but with some slight modification
3. It was hard to understand.

Controllers now define a single json file describing a tree of nodes.
The tree is composed of structural composites:

Sequence - do A, then B, then C (and so on)
Selector - try A, if it fails try B, then C (and so on)
Parallel - run A and B simultaneously, with configurable failure/success
policies and or looping behavior
Subplan - loop a child continiously

Along that we also have "Decorators". These are nodes that basically
check a condition (E.g.; do we have a combat target). These decorators
can be used to gate behavior and are re-useable across behavior trees.
They also have a concept known as "Observers". Which lets them cancel
lower priority behavior in case their condition changes (Which we check
whenever a signal fires that fits that specific decorator). This makes
the AI much more responsive to change in environment.

For behaviors, we still use the ai_behavior datums. These are the actual
behaviors such as "Move to X", "Attack X". The only major change is that
these can no longer sleep() since they now run in the ai_controller.

Lastly, we now also have subtrees, except now they are essentially
pieces of behavior tree that can be re-used, or even overriden at
runtime or as a variable. Allowing for making modular AI made out of
several smaller trees.

You can set variables on these nodes directly via the extension (see
below), which should reduce the need to make subtypes of behaviors by a
lot. All of these vars are saved on the JSON and will be applied at
runtime.

If you are using subtrees, you can also assign "bindings" to these
variables, which will allow instances of the subtree to override those
variables.

Since a tree structure with variables becomes hard to parse in a JSON,
I've made a VSCode extension to edit these JSONs:

https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg
https://github.com/CabinetOnFire/BehaviorTreeG

<img width="1795" height="1268" alt="image"
src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6"
/>

This extension allows you to edit the behavior tree JSONs, and browse
through all the behaviors/decorators/subtrees we have

If you'd like more info on how to build these AI check out the
learn_ai.md. I will also make a tutorial to go over more depth on what
the system offers because I kind of suck at doing technical write-ups.

Targetting has been changed to. I've made a new acquire_targets behavior
that takes a target_source (what am I targetting) and
targetting_strategy (what does the candidate need to fulfill to be
considered a target). This allows us to make composites targetting
combinations to reduce the amount of specific find_and_set esque
behaviors we had before. Not everything is ported to this system but
that would be a longer term goal.

I've added a new build_bt script that converts all the behavior tree
JSONs into compiled versions. Why is this needed? Because I wanted to
keep using defines in behavior trees, so we need a way to convert this
into literal values before we send it to DM. This script runs on compile
and should also run in CI (If I didn't fuck that up!). This saves to a
new build/ folder.

I've ported every single AI in the game to this system (except raptors,
Kobsa is working on those so should be in soon!), so I do expect some
bugs to come out of this. But I also fixed some issues that have
probably been in the game for a long time such as:
- Fixed penguins being unable to fish
- Fixed bileworms not being able to devour people
- Fixes goldgrubs not grubbing gold (they could not mine!)
- Lizards actually eat food they find

Either way, I'd reccomend a long TM on this.

1. (Hopefully) a better development experience for making AI
2. Less copy-paste for behaviors, we should be able to re-use more
pieces to make behavior
3. Behavior trees is a more common pattern in making AI, so it should be
easier to find resources to find out how to do things.

🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix
refactor: Replaces our AI system with behavior trees, porting all
datum/ai to it
/🆑

I will add this PR with more details down the line. I think I got the
big picture but its a big PR, so sorry if I missed something important.

---------

Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
2026-08-15 10:33:57 -06:00

729 lines
28 KiB
Plaintext

//Brain traumas that are rare and/or somewhat beneficial;
//they are the easiest to cure, which means that if you want
//to keep them, you can't cure your other traumas
/datum/brain_trauma/special
abstract_type = /datum/brain_trauma/special
/datum/brain_trauma/special/godwoken
name = "Godwoken Syndrome"
desc = "Patient occasionally and uncontrollably channels an eldritch god when speaking."
scan_desc = "god delusion"
symptoms = "Occasionally utters phrases or commands in a commanding tone, often accompanied by a sense of divine authority. \
These utterances can influence the behavior of others, compelling them to act in accordance with the spoken words."
gain_text = span_notice("You feel a higher power inside your mind...")
lose_text = span_warning("The divine presence leaves your head, no longer interested.")
/datum/brain_trauma/special/godwoken/on_life(seconds_per_tick)
..()
if(SPT_PROB(2, seconds_per_tick))
if(prob(33) && (owner.IsStun() || owner.IsParalyzed() || owner.IsUnconscious()))
speak("unstun", TRUE)
else if(prob(60) && owner.health <= owner.crit_threshold)
speak("heal", TRUE)
else if(prob(30) && owner.combat_mode)
speak("aggressive")
else
speak("neutral", prob(25))
/datum/brain_trauma/special/godwoken/on_gain()
ADD_TRAIT(owner, TRAIT_HOLY, TRAUMA_TRAIT)
. = ..()
/datum/brain_trauma/special/godwoken/on_lose()
REMOVE_TRAIT(owner, TRAIT_HOLY, TRAUMA_TRAIT)
..()
/datum/brain_trauma/special/godwoken/proc/speak(type, include_owner = FALSE)
var/message
switch(type)
if("unstun")
message = pick_list_replacements(BRAIN_DAMAGE_FILE, "god_unstun")
if("heal")
message = pick_list_replacements(BRAIN_DAMAGE_FILE, "god_heal")
if("neutral")
message = pick_list_replacements(BRAIN_DAMAGE_FILE, "god_neutral")
if("aggressive")
message = pick_list_replacements(BRAIN_DAMAGE_FILE, "god_aggressive")
else
message = pick_list_replacements(BRAIN_DAMAGE_FILE, "god_neutral")
playsound(get_turf(owner), 'sound/effects/magic/clockwork/invoke_general.ogg', 200, TRUE, 5)
voice_of_god(message, owner, list("colossus","yell"), 2.5, include_owner, name, TRUE)
/datum/brain_trauma/special/bluespace_prophet
name = "Bluespace Prophecy"
desc = "Patient can sense the bob and weave of bluespace around them, showing them passageways no one else can see."
scan_desc = "bluespace attunement"
symptoms = "Gains the ability to perceive hidden pathways through bluespace, allowing for spontaneous creation of temporary portals \
that connect two distant locations. To the average eye, the patient appears to disappear into thin air, only to reappear elsewhere nearby."
gain_text = span_notice("You feel the bluespace pulsing around you...")
lose_text = span_warning("The faint pulsing of bluespace fades into silence.")
/// Cooldown so we can't teleport literally everywhere on a whim
COOLDOWN_DECLARE(portal_cooldown)
/datum/brain_trauma/special/bluespace_prophet/on_life(seconds_per_tick)
if(!COOLDOWN_FINISHED(src, portal_cooldown))
return
COOLDOWN_START(src, portal_cooldown, 10 SECONDS)
var/list/turf/possible_turfs = list()
for(var/turf/T as anything in RANGE_TURFS(8, owner))
if(T.density)
continue
var/clear = TRUE
for(var/obj/O in T)
if(O.density)
clear = FALSE
break
if(clear)
possible_turfs += T
if(!LAZYLEN(possible_turfs))
return
var/turf/first_turf = pick(possible_turfs)
if(!first_turf)
return
possible_turfs -= (possible_turfs & range(first_turf, 3))
var/turf/second_turf = pick(possible_turfs)
if(!second_turf)
return
var/obj/effect/client_image_holder/bluespace_stream/first = new(first_turf, owner)
var/obj/effect/client_image_holder/bluespace_stream/second = new(second_turf, owner)
first.linked_to = second
second.linked_to = first
/obj/effect/client_image_holder/bluespace_stream
name = "bluespace stream"
desc = "You see a hidden pathway through bluespace..."
image_icon = 'icons/effects/effects.dmi'
image_state = "bluestream"
image_layer = ABOVE_MOB_LAYER
var/obj/effect/client_image_holder/bluespace_stream/linked_to
/obj/effect/client_image_holder/bluespace_stream/Initialize(mapload, list/mobs_which_see_us)
. = ..()
QDEL_IN(src, 30 SECONDS)
/obj/effect/client_image_holder/bluespace_stream/generate_image()
. = ..()
apply_wibbly_filters(.)
/obj/effect/client_image_holder/bluespace_stream/Destroy()
if(!QDELETED(linked_to))
qdel(linked_to)
linked_to = null
return ..()
/obj/effect/client_image_holder/bluespace_stream/attack_hand(mob/user, list/modifiers)
. = ..()
if(.)
return
if(!(user in who_sees_us) || !linked_to)
return
var/slip_in_message = pick("slides sideways in an odd way, and disappears", "jumps into an unseen dimension",\
"sticks one leg straight out, wiggles [user.p_their()] foot, and is suddenly gone", "stops, then blinks out of reality", \
"is pulled into an invisible vortex, vanishing from sight")
var/slip_out_message = pick("silently fades in", "leaps out of thin air","appears", "walks out of an invisible doorway",\
"slides out of a fold in spacetime")
to_chat(user, span_notice("You try to align with the bluespace stream..."))
if(!do_after(user, 2 SECONDS, target = src))
return
var/turf/source_turf = get_turf(src)
var/turf/destination_turf = get_turf(linked_to)
new /obj/effect/temp_visual/bluespace_fissure(source_turf)
new /obj/effect/temp_visual/bluespace_fissure(destination_turf)
user.visible_message(span_warning("[user] [slip_in_message]."), ignored_mobs = user)
if(do_teleport(user, destination_turf, no_effects = TRUE))
user.visible_message(span_warning("[user] [slip_out_message]."), span_notice("...and find your way to the other side."))
else
user.visible_message(span_warning("[user] [slip_out_message], ending up exactly where they left."), span_notice("...and find yourself where you started?"))
/obj/effect/client_image_holder/bluespace_stream/attack_tk(mob/user)
to_chat(user, span_warning("\The [src] actively rejects your mind, and the bluespace energies surrounding it disrupt your telekinesis!"))
return COMPONENT_CANCEL_ATTACK_CHAIN
/datum/brain_trauma/special/quantum_alignment
name = "Quantum Alignment"
desc = "Patient is prone to frequent spontaneous quantum entanglement, against all odds, causing spatial anomalies."
scan_desc = "quantum alignment"
symptoms = "Frequently experiences spontaneous quantum entanglement with nearby objects or beings, \
resulting in sudden and unpredictable teleportation events that connect the patient to the entangled target."
gain_text = span_notice("You feel faintly connected to everything around you...")
lose_text = span_warning("You no longer feel connected to your surroundings.")
var/atom/linked_target = null
var/linked = FALSE
var/returning = FALSE
/// Cooldown for snapbacks
COOLDOWN_DECLARE(snapback_cooldown)
/datum/brain_trauma/special/quantum_alignment/on_life(seconds_per_tick)
if(linked)
if(QDELETED(linked_target))
linked_target = null
linked = FALSE
return
if(!returning && COOLDOWN_FINISHED(src, snapback_cooldown))
start_snapback()
return
if(SPT_PROB(2, seconds_per_tick))
try_entangle()
/datum/brain_trauma/special/quantum_alignment/proc/try_entangle()
//Check for pulled mobs
if(ismob(owner.pulling))
entangle(owner.pulling)
return
//Check for adjacent mobs
for(var/mob/living/L in oview(1, owner))
if(owner.Adjacent(L))
entangle(L)
return
//Check for pulled objects
if(isobj(owner.pulling))
entangle(owner.pulling)
return
//Check main hand
var/obj/item/held_item = owner.get_active_held_item()
if(held_item && !(HAS_TRAIT(held_item, TRAIT_NODROP)))
entangle(held_item)
return
//Check off hand
held_item = owner.get_inactive_held_item()
if(held_item && !(HAS_TRAIT(held_item, TRAIT_NODROP)))
entangle(held_item)
return
//Just entangle with the turf
entangle(get_turf(owner))
/datum/brain_trauma/special/quantum_alignment/proc/entangle(atom/target)
to_chat(owner, span_notice("You start feeling a strong sense of connection to [target]."))
linked_target = target
linked = TRUE
COOLDOWN_START(src, snapback_cooldown, rand(45 SECONDS, 10 MINUTES))
/datum/brain_trauma/special/quantum_alignment/proc/start_snapback()
if(QDELETED(linked_target))
linked_target = null
linked = FALSE
return
to_chat(owner, span_warning("Your connection to [linked_target] suddenly feels extremely strong... you can feel it pulling you!"))
owner.playsound_local(owner, 'sound/effects/magic/lightning_chargeup.ogg', 75, FALSE)
returning = TRUE
addtimer(CALLBACK(src, PROC_REF(snapback)), 10 SECONDS)
/datum/brain_trauma/special/quantum_alignment/proc/snapback()
returning = FALSE
if(QDELETED(linked_target))
to_chat(owner, span_notice("The connection fades abruptly, and the pull with it."))
linked_target = null
linked = FALSE
return
to_chat(owner, span_warning("You're pulled through spacetime!"))
do_teleport(owner, get_turf(linked_target), null, channel = TELEPORT_CHANNEL_QUANTUM)
owner.playsound_local(owner, 'sound/effects/magic/repulse.ogg', 100, FALSE)
linked_target = null
linked = FALSE
/datum/brain_trauma/special/psychotic_brawling
name = "Violent Psychosis"
desc = "Patient fights in unpredictable ways, ranging from helping his target to hitting them with brutal strength."
scan_desc = "violent psychosis"
symptoms = "Exhibits erratic and potentially violent behavior when in physical contact with others, \
often accidentally attacking those they intend to offer a hand, or hugging those who they mean to strike."
gain_text = span_warning("You feel unhinged...")
lose_text = span_notice("You feel more balanced.")
/// The martial art we teach
var/datum/martial_art/psychotic_brawling/psychotic_brawling
/datum/brain_trauma/special/psychotic_brawling/on_gain()
. = ..()
psychotic_brawling = new(src)
psychotic_brawling.locked_to_use = TRUE
psychotic_brawling.teach(owner)
/datum/brain_trauma/special/psychotic_brawling/on_lose()
. = ..()
QDEL_NULL(psychotic_brawling)
/datum/brain_trauma/special/psychotic_brawling/bath_salts
name = "Chemical Violent Psychosis"
known_trauma = FALSE
/datum/brain_trauma/special/tenacity
name = "Tenacity"
desc = "Patient is psychologically unaffected by pain and injuries, and can remain standing far longer than a normal person."
scan_desc = "traumatic neuropathy"
symptoms = "Exhibits a remarkable resistance to pain and physical trauma, \
allowing them to sustain severe injuries that would incapacitate an otherwise normal individual."
gain_text = span_warning("You suddenly stop feeling pain.")
lose_text = span_warning("You realize you can feel pain again.")
/datum/brain_trauma/special/tenacity/on_gain()
owner.add_traits(list(TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_ANALGESIA), TRAUMA_TRAIT)
. = ..()
/datum/brain_trauma/special/tenacity/on_lose()
owner.remove_traits(list(TRAIT_NOSOFTCRIT, TRAIT_NOHARDCRIT, TRAIT_ANALGESIA), TRAUMA_TRAIT)
..()
/datum/brain_trauma/special/death_whispers
name = "Functional Cerebral Necrosis"
desc = "Patient's brain is stuck in a functional near-death state, causing occasional moments of lucid hallucinations, which are often interpreted as the voices of the dead."
scan_desc = "chronic functional necrosis"
symptoms = "Experiences intermittent auditory hallucinations characterized by whispering voices, \
which are often perceived as communications from the deceased."
gain_text = span_warning("You feel dead inside.")
lose_text = span_notice("You feel alive again.")
var/active = FALSE
/datum/brain_trauma/special/death_whispers/on_life()
..()
if(!active && prob(2))
whispering()
/datum/brain_trauma/special/death_whispers/on_lose()
if(active)
cease_whispering()
..()
/datum/brain_trauma/special/death_whispers/proc/whispering()
ADD_TRAIT(owner, TRAIT_SIXTHSENSE, TRAUMA_TRAIT)
active = TRUE
addtimer(CALLBACK(src, PROC_REF(cease_whispering)), rand(5 SECONDS, 30 SECONDS))
/datum/brain_trauma/special/death_whispers/proc/cease_whispering()
REMOVE_TRAIT(owner, TRAIT_SIXTHSENSE, TRAUMA_TRAIT)
active = FALSE
/datum/brain_trauma/special/existential_crisis
name = "Existential Crisis"
desc = "Patient's hold on reality becomes faint, causing occasional bouts of non-existence."
scan_desc = "existential crisis"
symptoms = "Experiences sporadic episodes of \"non-existence\", during which the patient temporarily fades out of reality, \
becoming intangible and invisible to others. Often accompanied by feelings of detachment, depression, and disorientation."
gain_text = span_warning("You feel less real.")
lose_text = span_notice("You feel more substantial again.")
var/obj/effect/abstract/sync_holder/veil/veil
/// A cooldown to prevent constantly erratic dolphining through the fabric of reality
COOLDOWN_DECLARE(crisis_cooldown)
/datum/brain_trauma/special/existential_crisis/on_life(seconds_per_tick)
..()
if(!veil && COOLDOWN_FINISHED(src, crisis_cooldown) && SPT_PROB(1.5, seconds_per_tick))
if(isturf(owner.loc))
fade_out()
/datum/brain_trauma/special/existential_crisis/on_lose()
if(veil)
fade_in()
..()
/datum/brain_trauma/special/existential_crisis/proc/fade_out()
if(veil)
return
var/duration = rand(5 SECONDS, 45 SECONDS)
veil = new(owner.drop_location())
to_chat(owner, span_warning("[pick(list(
"Do you even exist?",
"To be or not to be...",
"Why exist?",
"You simply fade away.",
"You stop keeping it real.",
"You stop thinking for a moment. Therefore you are not.",
))]"))
owner.forceMove(veil)
COOLDOWN_START(src, crisis_cooldown, 1 MINUTES)
addtimer(CALLBACK(src, PROC_REF(fade_in)), duration)
/datum/brain_trauma/special/existential_crisis/proc/fade_in()
QDEL_NULL(veil)
to_chat(owner, span_notice("You fade back into reality."))
COOLDOWN_START(src, crisis_cooldown, 1 MINUTES)
//base sync holder is in desynchronizer.dm
/obj/effect/abstract/sync_holder/veil
name = "non-existence"
desc = "Existence is just a state of mind."
/datum/brain_trauma/special/beepsky
name = "Criminal"
desc = "Patient seems to be a criminal."
scan_desc = "criminal mind"
gain_text = span_warning("Justice is coming for you.")
lose_text = span_notice("You were absolved for your crimes.")
random_gain = FALSE
known_trauma = FALSE
/// A ref to our fake beepsky image that we chase the owner with
var/obj/effect/client_image_holder/securitron/beepsky
/datum/brain_trauma/special/beepsky/Destroy()
QDEL_NULL(beepsky)
return ..()
/datum/brain_trauma/special/beepsky/on_gain()
create_securitron()
return ..()
/datum/brain_trauma/special/beepsky/proc/create_securitron()
QDEL_NULL(beepsky)
var/turf/where = locate(owner.x + pick(-12, 12), owner.y + pick(-12, 12), owner.z)
beepsky = new(where, owner)
/datum/brain_trauma/special/beepsky/on_lose()
QDEL_NULL(beepsky)
return ..()
/datum/brain_trauma/special/beepsky/on_life()
if(QDELETED(beepsky) || !beepsky.loc || beepsky.z != owner.z)
if(prob(30))
create_securitron()
else
return
if(get_dist(owner, beepsky) >= 10 && prob(20))
create_securitron()
if(owner.stat != CONSCIOUS)
if(prob(20))
owner.playsound_local(beepsky, 'sound/mobs/non-humanoids/beepsky/iamthelaw.ogg', 50)
return
if(get_dist(owner, beepsky) <= 1)
owner.playsound_local(owner, 'sound/items/weapons/egloves.ogg', 50)
owner.visible_message(span_warning("[owner]'s body jerks as if it was shocked."), span_userdanger("You feel the fist of the LAW."))
owner.adjust_stamina_loss(rand(40, 70))
QDEL_NULL(beepsky)
if(prob(20) && get_dist(owner, beepsky) <= 8)
owner.playsound_local(beepsky, 'sound/mobs/non-humanoids/beepsky/criminal.ogg', 40)
/obj/effect/client_image_holder/securitron
name = "Securitron"
desc = "The LAW is coming."
image_icon = 'icons/mob/silicon/aibots.dmi'
image_state = "secbot-c"
/obj/effect/client_image_holder/securitron/Initialize(mapload)
. = ..()
name = pick("Officer Beepsky", "Officer Johnson", "Officer Pingsky")
START_PROCESSING(SSfastprocess, src)
/obj/effect/client_image_holder/securitron/Destroy()
STOP_PROCESSING(SSfastprocess,src)
return ..()
/obj/effect/client_image_holder/securitron/process()
if(prob(40))
return
var/mob/victim = pick(who_sees_us)
forceMove(get_step_towards(src, victim))
if(prob(5))
var/beepskys_cry = "Level 10 infraction alert!"
to_chat(victim, "[span_name("[name]")] exclaims, \"[span_robot("[beepskys_cry]")]")
if(victim.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat))
victim.create_chat_message(src, raw_message = beepskys_cry, spans = list(SPAN_ROBOT))
// Used by Veteran Security Advisor job.
/datum/brain_trauma/special/ptsd
name = "Combat PTSD"
desc = "The patient is experiencing PTSD stemming from past combat exposure, resulting in a lack of emotions. Additionally, they are experiencing mild hallucinations."
scan_desc = "PTSD"
symptoms = "Witnessed or experienced a traumatic, horrific, or potentially life threatening event, \
resulting in avoidance, intrusive thoughts, flashbacks, auditory hallucinations, \
emotional numbness, detachment from others, and heightened reactivity to stimuli - \
particularly in situations reminiscent of the traumatic event."
gain_text = span_warning("You're thrust back into the chaos of past! Explosions! Gunfire! Emotions, gone AWOL!")
lose_text = span_notice("You feel flashbacks of past fade, as your emotions return and mind clear.")
resilience = TRAUMA_RESILIENCE_ABSOLUTE
can_gain = TRUE
random_gain = FALSE
/// Our cooldown declare for causing hallucinations
COOLDOWN_DECLARE(ptsd_hallucinations)
var/list/ptsd_hallucinations_list = list(
/datum/hallucination/fake_sound/normal/boom,
/datum/hallucination/fake_sound/normal/distant_boom,
/datum/hallucination/stray_bullet,
/datum/hallucination/battle/gun/disabler,
/datum/hallucination/battle/gun/laser,
/datum/hallucination/battle/bomb,
/datum/hallucination/battle/e_sword,
/datum/hallucination/battle/harm_baton,
/datum/hallucination/battle/stun_prod,
)
/datum/brain_trauma/special/ptsd/on_life(seconds_per_tick)
if(owner.stat != CONSCIOUS)
return
if(!COOLDOWN_FINISHED(src, ptsd_hallucinations))
return
owner.cause_hallucination(pick(ptsd_hallucinations_list), "Caused by The Combat PTSD brain trauma")
COOLDOWN_START(src, ptsd_hallucinations, rand(10 SECONDS, 10 MINUTES))
/datum/brain_trauma/special/ptsd/on_gain()
owner.add_mood_event("combat_ptsd", /datum/mood_event/desentized)
owner.mob_mood?.mood_modifier -= 1 //Basically nothing can change your mood
owner.mob_mood?.set_sanity(SANITY_DISTURBED, override = TRUE) //Makes sanity on a unstable level unless cured
owner.apply_status_effect(/datum/status_effect/desensitized, REF(src), DESENSITIZED_THRESHOLD)
. = ..()
/datum/brain_trauma/special/ptsd/on_lose()
owner.clear_mood_event("combat_ptsd")
owner.mob_mood?.mood_modifier += 1
owner.mob_mood?.set_sanity(SANITY_GREAT, override = TRUE)
owner.remove_status_effect(/datum/status_effect/desensitized, REF(src))
return ..()
/datum/brain_trauma/special/primal_instincts
name = "Feral Instincts"
desc = "Patient's mind is stuck in a primal state, causing them to act on instinct rather than reason."
scan_desc = "ferality"
symptoms = "Rarely experiences episodes where higher cognitive functions are suppressed, \
resulting in behavior driven primarily by primal instincts. During these episodes, \
the patient may exhibit increased aggression, territoriality, and a focus on basic survival needs."
gain_text = span_warning("Your pupils dilate, and it becomes harder to think straight.")
lose_text = span_notice("Your mind clears, and you feel more in control.")
resilience = TRAUMA_RESILIENCE_SURGERY
/// Tracks any existing AI controller, so we can restore it when we're cured
var/old_ai_controller_type
/datum/brain_trauma/special/primal_instincts/on_gain()
. = ..()
if(!isnull(owner.ai_controller))
old_ai_controller_type = owner.ai_controller.type
QDEL_NULL(owner.ai_controller)
owner.ai_controller = new /datum/ai_controller/monkey(owner)
owner.ai_controller.continue_processing_when_client = TRUE
owner.ai_controller.ai_traits |= RUN_WHILE_UNWATCHED
owner.ai_controller.force_ai_off()
/datum/brain_trauma/special/primal_instincts/on_lose(silent)
. = ..()
if(QDELING(owner))
return
QDEL_NULL(owner.ai_controller)
if(old_ai_controller_type)
owner.ai_controller = new old_ai_controller_type(owner)
owner.remove_language(/datum/language/monkey, UNDERSTOOD_LANGUAGE, TRAUMA_TRAIT)
/datum/brain_trauma/special/primal_instincts/on_life(seconds_per_tick)
if(isnull(owner.ai_controller))
qdel(src)
return
if(!SPT_PROB(3, seconds_per_tick))
return
owner.grant_language(/datum/language/monkey, UNDERSTOOD_LANGUAGE, TRAUMA_TRAIT)
owner.ai_controller.set_blackboard_key(BB_MONKEY_AGGRESSIVE, prob(75))
if(owner.ai_controller.ai_status == AI_STATUS_OFF)
owner.ai_controller.clear_forced_off()
owner.log_message("became controlled by monkey instincts ([owner.ai_controller.blackboard[BB_MONKEY_AGGRESSIVE] ? "aggressive" : "docile"])", LOG_ATTACK, color = "orange")
to_chat(owner, span_warning("You feel the urge to act on your primal instincts..."))
// extend original timer if we roll the effect while it's already ongoing
addtimer(CALLBACK(src, PROC_REF(primal_instincts_off)), rand(20 SECONDS, 40 SECONDS), TIMER_UNIQUE|TIMER_NO_HASH_WAIT|TIMER_OVERRIDE|TIMER_DELETE_ME)
/datum/brain_trauma/special/primal_instincts/proc/primal_instincts_off()
owner.ai_controller.force_ai_off()
owner.remove_language(/datum/language/monkey, UNDERSTOOD_LANGUAGE, TRAUMA_TRAIT)
to_chat(owner, span_green("The urge subsides."))
/datum/brain_trauma/special/axedoration
name = "Axe Delusions"
desc = "Patient feels an immense sense of duty towards protecting an axe and has hallucinations regarding it."
scan_desc = "object attachment"
gain_text = span_notice("You feel like protecting the fire axe is one of your greatest duties.")
lose_text = span_warning("You feel like you lost your sense of duty.")
resilience = TRAUMA_RESILIENCE_ABSOLUTE
random_gain = FALSE
known_trauma = FALSE
var/static/list/talk_lines = list(
"I'm proud of you.",
"I believe in you!",
"Do I bother you?",
"Praise me!",
"Fires burn.",
"We made it!",
"Mother, my body disgusts me.",
"There's a gap where we meet, where I end and you begin.",
"Humble yourself.",
)
var/static/list/hurt_lines = list(
"Ow!",
"Ouch!",
"Ack!",
"It burns!",
"Stop!",
"Arghh!",
"Please!",
"End it!",
"Cease!",
"Ah!",
)
/datum/brain_trauma/special/axedoration/on_life(seconds_per_tick)
if(owner.stat != CONSCIOUS)
return
if(!GLOB.bridge_axe)
if(SPT_PROB(0.5, seconds_per_tick))
to_chat(owner, span_warning("I've failed my duty..."))
owner.set_jitter_if_lower(5 SECONDS)
owner.set_stutter_if_lower(5 SECONDS)
if(SPT_PROB(20, seconds_per_tick))
owner.vomit(VOMIT_CATEGORY_DEFAULT)
return
var/atom/axe_location = get_axe_location()
if(!SPT_PROB(1.5, seconds_per_tick))
return
if(isliving(axe_location))
var/mob/living/axe_holder = axe_location
if(axe_holder == owner)
talk_tuah(pick(talk_lines))
return
var/datum/job/holder_job = axe_holder.mind?.assigned_role
if(holder_job && (/datum/job_department/command in holder_job.departments_list))
to_chat(owner, span_notice("I hope the axe is in good hands..."))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
return
to_chat(owner, span_warning("You start having a bad feeling..."))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_missing)
return
if(!isarea(axe_location))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_gone)
return
if(istype(axe_location, /area/station/command))
to_chat(owner, span_notice("You feel a sense of relief..."))
if(istype(GLOB.bridge_axe.loc, /obj/structure/fireaxecabinet))
return
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
return
to_chat(owner, span_warning("You start having a bad feeling..."))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_missing)
/datum/brain_trauma/special/axedoration/on_gain()
RegisterSignal(owner, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equip))
RegisterSignal(owner, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_unequip))
RegisterSignal(owner, COMSIG_MOB_EXAMINING, PROC_REF(on_examine))
if(!GLOB.bridge_axe)
axe_gone()
return ..()
RegisterSignal(GLOB.bridge_axe, COMSIG_QDELETING, PROC_REF(axe_gone))
if(istype(get_axe_location(), /area/station/command) && istype(GLOB.bridge_axe.loc, /obj/structure/fireaxecabinet))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_cabinet)
else if(owner.is_holding(GLOB.bridge_axe))
on_equip(owner, GLOB.bridge_axe)
else
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
RegisterSignal(GLOB.bridge_axe, COMSIG_ITEM_AFTERATTACK, PROC_REF(on_axe_attack))
return ..()
/datum/brain_trauma/special/axedoration/on_lose()
owner.clear_mood_event("fireaxe")
UnregisterSignal(owner, list(COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_MOB_EXAMINING))
if(GLOB.bridge_axe)
UnregisterSignal(GLOB.bridge_axe, COMSIG_ITEM_AFTERATTACK)
return ..()
/datum/brain_trauma/special/axedoration/proc/axe_gone(source)
SIGNAL_HANDLER
to_chat(owner, span_danger("You feel a great disturbance in the force."))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_gone)
owner.set_jitter_if_lower(15 SECONDS)
owner.set_stutter_if_lower(15 SECONDS)
/datum/brain_trauma/special/axedoration/proc/on_equip(source, obj/item/picked_up, slot)
SIGNAL_HANDLER
if(!istype(picked_up, /obj/item/fireaxe))
return
owner.set_jitter_if_lower(3 SECONDS)
if(picked_up == GLOB.bridge_axe)
to_chat(owner, span_hypnophrase("I have it. It's time to put it back."))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_held)
return
ADD_TRAIT(picked_up, TRAIT_NODROP, type)
to_chat(owner, span_warning("...This is not the one I'm looking after."))
owner.Immobilize(2 SECONDS)
addtimer(CALLBACK(src, PROC_REF(throw_faker), picked_up), 2 SECONDS)
/datum/brain_trauma/special/axedoration/proc/throw_faker(obj/item/faker)
REMOVE_TRAIT(faker, TRAIT_NODROP, type)
var/held_index = owner.get_held_index_of_item(faker)
if(!held_index)
return
to_chat(owner, span_warning("Be gone with you."))
owner.swap_hand(held_index, silent = TRUE)
var/turf/target_turf = get_ranged_target_turf(owner, owner.dir, faker.throw_range)
owner.throw_item(target_turf)
/datum/brain_trauma/special/axedoration/proc/on_unequip(datum/source, obj/item/dropped_item, force, new_location)
SIGNAL_HANDLER
if(dropped_item != GLOB.bridge_axe)
return
if(get_axe_location() == owner)
return
if(istype(new_location, /obj/structure/fireaxecabinet))
if(istype(get_area(new_location), /area/station/command))
to_chat(owner, span_nicegreen("Ah! Back where it belongs!"))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_cabinet)
INVOKE_ASYNC(owner, TYPE_PROC_REF(/mob, emote), "smile")
return
to_chat(owner, span_warning("Leaving it outside of command? Am I sure about that?"))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
return
to_chat(owner, span_warning("Should I really leave it here?"))
owner.add_mood_event("fireaxe", /datum/mood_event/axe_neutral)
/datum/brain_trauma/special/axedoration/proc/on_examine(mob/source, atom/target, list/examine_strings, list/examine_overrides)
SIGNAL_HANDLER
if(!istype(target, /obj/item/fireaxe))
return
if(target == GLOB.bridge_axe)
examine_strings += span_notice("It's the axe I've sworn to protect.")
else
examine_strings += span_warning("It's a simulacra, a fake axe made to fool the masses.")
/datum/brain_trauma/special/axedoration/proc/on_axe_attack(obj/item/axe, atom/target, mob/user, list/modifiers)
SIGNAL_HANDLER
if(user != owner)
return
talk_tuah(pick(hurt_lines))
/datum/brain_trauma/special/axedoration/proc/talk_tuah(sent_message = "Hello World.")
owner.Hear(GLOB.bridge_axe, owner.get_selected_language(), sent_message)
/datum/brain_trauma/special/axedoration/proc/get_axe_location()
if(!GLOB.bridge_axe)
return
var/atom/axe_loc = GLOB.bridge_axe.loc
while(!ismob(axe_loc) && !isarea(axe_loc) && !isnull(axe_loc))
axe_loc = axe_loc.loc
return axe_loc