diff --git a/code/__DEFINES/hud.dm b/code/__DEFINES/hud.dm
index 78d90c65ecb1..38e5693dcbe5 100644
--- a/code/__DEFINES/hud.dm
+++ b/code/__DEFINES/hud.dm
@@ -17,3 +17,9 @@
#define APPEARANCE_UI_IGNORE_ALPHA (RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR|RESET_ALPHA|PIXEL_SCALE)
/// Used for HUD objects
#define APPEARANCE_UI (RESET_COLOR|RESET_TRANSFORM|NO_CLIENT_COLOR|PIXEL_SCALE)
+
+// Notification action types
+#define NOTIFY_JUMP "jump"
+#define NOTIFY_ATTACK "attack"
+#define NOTIFY_ORBIT "orbit"
+#define NOTIFY_JOIN_XENO "join_xeno"
diff --git a/code/__DEFINES/sounds.dm b/code/__DEFINES/sounds.dm
index f01ddfc86792..a6bb381100e7 100644
--- a/code/__DEFINES/sounds.dm
+++ b/code/__DEFINES/sounds.dm
@@ -21,6 +21,7 @@
#define ITEM_EQUIP_VOLUME 50
//Reserved channels
+#define SOUND_CHANNEL_NOTIFY 1016
#define SOUND_CHANNEL_VOX 1017
#define SOUND_CHANNEL_MUSIC 1018
#define SOUND_CHANNEL_AMBIENCE 1019
diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm
index 139de9e59a35..c6b642974881 100644
--- a/code/_onclick/hud/_defines.dm
+++ b/code/_onclick/hud/_defines.dm
@@ -27,3 +27,10 @@
#define ui_ghost_slot3 "SOUTH:6,CENTER:0"
#define ui_ghost_slot4 "SOUTH:6,CENTER+1:0"
#define ui_ghost_slot5 "SOUTH:6,CENTER+2:0"
+
+//Upper-middle right (alerts)
+#define ui_alert1 "EAST-1:28,CENTER+5:27"
+#define ui_alert2 "EAST-1:28,CENTER+4:25"
+#define ui_alert3 "EAST-1:28,CENTER+3:23"
+#define ui_alert4 "EAST-1:28,CENTER+2:21"
+#define ui_alert5 "EAST-1:28,CENTER+1:19"
diff --git a/code/_onclick/hud/hud.dm b/code/_onclick/hud/hud.dm
index 7f9ad85e154e..392f3ae9a060 100644
--- a/code/_onclick/hud/hud.dm
+++ b/code/_onclick/hud/hud.dm
@@ -400,3 +400,40 @@
zone_sel.color = ui_color
zone_sel.update_icon(mymob)
static_inventory += zone_sel
+
+// Re-render all alerts - also called in /datum/hud/show_hud() because it's needed there
+/datum/hud/proc/reorganize_alerts(mob/viewmob)
+ var/mob/screenmob = viewmob || mymob
+ if(!screenmob.client)
+ return
+ var/list/alerts = mymob.alerts
+ if(!length(alerts))
+ return FALSE
+ if(!hud_shown)
+ for(var/category in alerts)
+ var/atom/movable/screen/alert/alert = alerts[category]
+ screenmob.client.screen -= alert
+ return TRUE
+ var/c = 0
+ for(var/category in alerts)
+ var/atom/movable/screen/alert/alert = alerts[category]
+ c++
+ switch(c)
+ if(1)
+ . = ui_alert1
+ if(2)
+ . = ui_alert2
+ if(3)
+ . = ui_alert3
+ if(4)
+ . = ui_alert4
+ if(5)
+ . = ui_alert5 // Right now there's 5 slots
+ else
+ . = ""
+ alert.screen_loc = .
+ screenmob.client.screen |= alert
+ if(!viewmob)
+ for(var/obs in mymob.observers)
+ reorganize_alerts(obs)
+ return TRUE
diff --git a/code/datums/effects/neurotoxin.dm b/code/datums/effects/neurotoxin.dm
index 836fccf49ca3..f5489f6f8578 100644
--- a/code/datums/effects/neurotoxin.dm
+++ b/code/datums/effects/neurotoxin.dm
@@ -128,8 +128,7 @@
switch(rand(0, 100))
if(0 to 5)
if(hallu_area)
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("[victim] has experienced a rare neuro-induced 'Schizo Lurker Pounce' hallucination (5% chance) at \the [hallu_area]" + " [OBSERVER_JMP(observer, victim)]"))
+ notify_ghosts(header = "Hallucinating!", message = "[victim] has experienced a rare neuro-induced 'Schizo Lurker Pounce' hallucination (5% chance) at [hallu_area].", source = victim, action = NOTIFY_ORBIT)
playsound_client(victim?.client,pick('sound/voice/alien_pounce.ogg','sound/voice/alien_pounce.ogg'))
victim.KnockDown(3)
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound_client), victim.client,"alien_claw_flesh"), 1 SECONDS)
@@ -141,8 +140,7 @@
victim.emote("pain")
if(6 to 10)
if(hallu_area)
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("[victim] has experienced a rare neuro-induced 'OB' hallucination (4% chance) at \the [hallu_area]" + " [OBSERVER_JMP(observer, victim)]"))
+ notify_ghosts(header = "Hallucinating!", message = "[victim] has experienced a rare neuro-induced 'OB' hallucination (4% chance) at [hallu_area].", source = victim, action = NOTIFY_ORBIT)
playsound_client(victim.client,'sound/effects/ob_alert.ogg')
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(playsound_client), victim.client,'sound/weapons/gun_orbital_travel.ogg'), 2 SECONDS)
if(11 to 16)
@@ -150,8 +148,7 @@
victim.KnockDown(1)
if(17 to 24)
if(hallu_area)
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("[victim] has experienced a rare neuro-induced 'Fake CAS firemission' hallucination (7% chance) at \the [hallu_area]" + " [OBSERVER_JMP(observer, victim)]"))
+ notify_ghosts(header = "Hallucinating!", message = "[victim] has experienced a rare neuro-induced 'Fake CAS firemission' hallucination (7% chance) at [hallu_area]", source = victim, action = NOTIFY_ORBIT)
hallucination_fakecas_sequence(victim) //Not gonna spam a billion timers for this one so outsourcing to a proc with sleeps is a better async solution
if(25 to 42)
to_chat(victim,SPAN_HIGHDANGER("A SHELL IS ABOUT TO IMPACT [pick(SPAN_UNDERLINE("TOWARDS THE [pick("WEST","EAST","SOUTH","NORTH")]"),SPAN_UNDERLINE("RIGHT ONTOP OF YOU!"))]!"))
diff --git a/code/modules/admin/verbs/freeforghosts.dm b/code/modules/admin/verbs/freeforghosts.dm
index a2f3912030e5..24c261ee18f3 100644
--- a/code/modules/admin/verbs/freeforghosts.dm
+++ b/code/modules/admin/verbs/freeforghosts.dm
@@ -6,22 +6,27 @@
to_chat(src, "Only staff members may use this.")
return
- free_for_ghosts(M)
+ free_for_ghosts(M, notify = TRUE)
message_admins("[key_name_admin(usr)] freed [key_name(M)] for ghosts to take.")
-/client/proc/free_for_ghosts(mob/living/M in GLOB.living_mob_list)
+/client/proc/free_for_ghosts(mob/living/M in GLOB.living_mob_list, notify)
if(!ismob(M))
return
- M.free_for_ghosts()
+ M.free_for_ghosts(notify)
-/mob/proc/free_for_ghosts()
+/mob/proc/free_for_ghosts(notify)
if(mind || client)
ghostize(FALSE)
GLOB.freed_mob_list |= WEAKREF(src)
+ if(!notify)
+ return
+
+ notify_ghosts(header = "Freed Mob", message = "A mob is now available for ghosts. Name: [real_name], Job: [job ? job : ""]", enter_link = "claim_freed=[REF(src)]", source = src, action = NOTIFY_ORBIT)
+
/client/proc/free_all_mobs_in_view()
set name = "Free All Mobs"
set category = "Admin.InView"
@@ -34,6 +39,6 @@
return
for(var/mob/living/M in view())
- free_for_ghosts(M)
+ free_for_ghosts(M, notify = FALSE)
message_admins(WRAP_STAFF_LOG(usr, "freed all mobs in [get_area(usr)] ([usr.x],[usr.y],[usr.z])"), usr.x, usr.y, usr.z)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 3722b32fb2b4..a7149c07d3e7 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -436,6 +436,9 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
//if(prefs.window_skin & TOGGLE_WINDOW_SKIN)
// set_night_skin()
+ if(!tooltips && prefs.tooltips)
+ tooltips = new(src)
+
load_player_data()
view = world_view_size
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 4f1161709657..76323a19ac8c 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -234,6 +234,9 @@ var/const/MAX_SAVE_SLOTS = 10
/// if this client has custom cursors enabled
var/custom_cursors = TRUE
+ /// if this client has tooltips enabled
+ var/tooltips = TRUE
+
/datum/preferences/New(client/C)
key_bindings = deepCopyList(GLOB.hotkey_keybinding_list_by_key) // give them default keybinds and update their movement keys
macros = new(C, src)
@@ -573,6 +576,7 @@ var/const/MAX_SAVE_SLOTS = 10
dat += "Ambient Occlusion: [toggle_prefs & TOGGLE_AMBIENT_OCCLUSION ? "Enabled" : "Disabled"]
"
dat += "Fit Viewport: [auto_fit_viewport ? "Auto" : "Manual"]
"
dat += "Adaptive Zoom: [adaptive_zoom ? "[adaptive_zoom * 2]x" : "Disabled"]
"
+ dat += "Tooltips: [tooltips ? "Enabled" : "Disabled"]
"
dat += "tgui Window Mode: [(tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]
"
dat += "tgui Window Placement: [(tgui_lock) ? "Primary monitor" : "Free (default)"]
"
dat += "Play Admin Midis: [(toggles_sound & SOUND_MIDI) ? "Yes" : "No"]
"
@@ -1862,6 +1866,17 @@ var/const/MAX_SAVE_SLOTS = 10
adaptive_zoom = 0
owner?.adaptive_zoom()
+ if("tooltips")
+ tooltips = !tooltips
+ save_preferences()
+
+ if(!tooltips)
+ closeToolTip()
+ return
+
+ if(!owner.tooltips)
+ owner.tooltips = new(owner)
+
if("inputstyle")
var/result = tgui_alert(user, "Which input style do you want?", "Input Style", list("Modern", "Legacy"))
if(!result)
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 0f482fa7f894..7d9a67c455a9 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -193,6 +193,7 @@
S["custom_cursors"] >> custom_cursors
S["autofit_viewport"] >> auto_fit_viewport
S["adaptive_zoom"] >> adaptive_zoom
+ S["tooltips"] >> tooltips
//Sanitize
ooccolor = sanitize_hexcolor(ooccolor, CONFIG_GET(string/ooc_color_default))
@@ -225,6 +226,7 @@
no_radial_labels_preference = sanitize_integer(no_radial_labels_preference, FALSE, TRUE, FALSE)
auto_fit_viewport = sanitize_integer(auto_fit_viewport, FALSE, TRUE, TRUE)
adaptive_zoom = sanitize_integer(adaptive_zoom, 0, 2, 0)
+ tooltips = sanitize_integer(tooltips, FALSE, TRUE, TRUE)
synthetic_name = synthetic_name ? sanitize_text(synthetic_name, initial(synthetic_name)) : initial(synthetic_name)
synthetic_type = sanitize_inlist(synthetic_type, PLAYER_SYNTHS, initial(synthetic_type))
diff --git a/code/modules/cm_aliens/structures/special/pylon_core.dm b/code/modules/cm_aliens/structures/special/pylon_core.dm
index 593134642198..65d4a1c9168e 100644
--- a/code/modules/cm_aliens/structures/special/pylon_core.dm
+++ b/code/modules/cm_aliens/structures/special/pylon_core.dm
@@ -236,7 +236,7 @@
last_surge_time = world.time
linked_hive.stored_larva++
linked_hive.hijack_burrowed_left--
- announce_dchat("The hive has gained another burrowed larva! Use the Join As Xeno verb to take it.", src)
+ notify_ghosts(header = "Claim Xeno", message = "The Hive has gained another burrowed larva! Click to take it.", source = src, action = NOTIFY_JOIN_XENO, enter_link = "join_xeno")
if(surge_cooldown > 30 SECONDS) //mostly for sanity purposes
surge_cooldown = surge_cooldown - surge_incremental_reduction //ramps up over time
if(linked_hive.hijack_burrowed_left < 1)
diff --git a/code/modules/cm_marines/overwatch.dm b/code/modules/cm_marines/overwatch.dm
index 26de42ad99ec..2dff476a7b3e 100644
--- a/code/modules/cm_marines/overwatch.dm
+++ b/code/modules/cm_marines/overwatch.dm
@@ -792,7 +792,8 @@
return
var/ob_name = lowertext(almayer_orbital_cannon.tray.warhead.name)
- announce_dchat("\A [ob_name] targeting [A.name] has been fired!", T)
+ var/mutable_appearance/warhead_appearance = mutable_appearance(almayer_orbital_cannon.tray.warhead.icon, almayer_orbital_cannon.tray.warhead.icon_state)
+ notify_ghosts(header = "Bombardment Inbound", message = "\A [ob_name] targeting [A.name] has been fired!", source = T, alert_overlay = warhead_appearance, extra_large = TRUE)
message_admins(FONT_SIZE_HUGE("ALERT: [key_name(user)] fired an orbital bombardment in [A.name] for squad '[current_squad]' [ADMIN_JMP(T)]"))
log_attack("[key_name(user)] fired an orbital bombardment in [A.name] for squad '[current_squad]'")
diff --git a/code/modules/maptext_alerts/screen_alerts.dm b/code/modules/maptext_alerts/screen_alerts.dm
index e96b436bde21..6d251080e87b 100644
--- a/code/modules/maptext_alerts/screen_alerts.dm
+++ b/code/modules/maptext_alerts/screen_alerts.dm
@@ -116,3 +116,133 @@
if(LAZYLEN(player.screen_texts))
player.screen_texts[1].play_to_client() // Theres more?
+/**
+ * Proc to create or update an alert. Returns the alert if the alert is new or updated, 0 if it was thrown already
+ * category is a text string. Each mob may only have one alert per category; the previous one will be replaced
+ * path is a type path of the actual alert type to throw
+ * severity is an optional number that will be placed at the end of the icon_state for this alert
+ * For example, high pressure's icon_state is "highpressure" and can be serverity 1 or 2 to get "highpressure1" or "highpressure2"
+ * new_master is optional and sets the alert's icon state to "template" in the ui_style icons with the master as an overlay.
+ * Clicks are forwarded to master
+ * Override makes it so the alert is not replaced until cleared by a clear_alert with clear_override, and it's used for hallucinations.
+ */
+/mob/proc/throw_alert(category, type, severity, obj/new_master, override = FALSE)
+ if(!category || QDELETED(src))
+ return
+
+ var/atom/movable/screen/alert/thealert
+ if(alerts[category])
+ thealert = alerts[category]
+ if(thealert.override_alerts)
+ return FALSE
+ if(new_master && new_master != thealert.master)
+ WARNING("[src] threw alert [category] with new_master [new_master] while already having that alert with master [thealert.master]")
+
+ clear_alert(category)
+ return .()
+ else if(thealert.type != type)
+ clear_alert(category)
+ return .()
+ else if(!severity || severity == thealert.severity)
+ if(thealert.timeout)
+ clear_alert(category)
+ return .()
+ else //no need to update
+ return FALSE
+ else
+ thealert = new type()
+ thealert.override_alerts = override
+ if(override)
+ thealert.timeout = null
+ thealert.owner = src
+
+ if(new_master)
+ var/old_layer = new_master.layer
+ var/old_plane = new_master.plane
+ new_master.layer = FLOAT_LAYER
+ new_master.plane = FLOAT_PLANE
+ thealert.overlays += new_master
+ new_master.layer = old_layer
+ new_master.plane = old_plane
+ thealert.icon_state = "template" // We'll set the icon to the client's ui pref in reorganize_alerts()
+ thealert.master = new_master
+ else
+ thealert.icon_state = "[initial(thealert.icon_state)][severity]"
+ thealert.severity = severity
+
+ alerts[category] = thealert
+ if(client && hud_used)
+ hud_used.reorganize_alerts()
+ thealert.transform = matrix(32, 6, MATRIX_TRANSLATE)
+ animate(thealert, transform = matrix(), time = 2.5, easing = CUBIC_EASING)
+
+ if(thealert.timeout)
+ addtimer(CALLBACK(src, PROC_REF(alert_timeout), thealert, category), thealert.timeout)
+ thealert.timeout = world.time + thealert.timeout - world.tick_lag
+ return thealert
+
+/mob/proc/alert_timeout(atom/movable/screen/alert/alert, category)
+ if(alert.timeout && alerts[category] == alert && world.time >= alert.timeout)
+ clear_alert(category)
+
+// Proc to clear an existing alert.
+/mob/proc/clear_alert(category, clear_override = FALSE)
+ var/atom/movable/screen/alert/alert = alerts[category]
+ if(!alert)
+ return FALSE
+ if(alert.override_alerts && !clear_override)
+ return FALSE
+
+ alerts -= category
+ if(client && hud_used)
+ hud_used.reorganize_alerts()
+ client.screen -= alert
+ qdel(alert)
+
+/atom/movable/screen/alert
+ icon = 'icons/mob/screen_alert.dmi'
+ icon_state = "default"
+ name = "Alert"
+ desc = "Something seems to have gone wrong with this alert, so report this bug please"
+ mouse_opacity = MOUSE_OPACITY_ICON
+ /// If set to a number, this alert will clear itself after that many deciseconds
+ var/timeout = 0
+ var/severity = 0
+ var/alerttooltipstyle = ""
+ /// If it is overriding other alerts of the same type
+ var/override_alerts = FALSE
+ /// Alert owner
+ var/mob/owner
+
+/atom/movable/screen/alert/MouseEntered(location,control,params)
+ . = ..()
+ if(!QDELETED(src))
+ openToolTip(usr, src, params, title = name, content = desc, theme = alerttooltipstyle)
+
+/atom/movable/screen/alert/notify_action
+ name = "Notification"
+ desc = "A new notification. You can enter it."
+ icon_state = "template"
+ timeout = 15 SECONDS
+ var/atom/target = null
+ var/action = NOTIFY_JUMP
+
+/atom/movable/screen/alert/notify_action/Click()
+ var/mob/dead/observer/ghost_user = usr
+ if(!istype(ghost_user) || usr != owner)
+ return
+ if(!ghost_user.client)
+ return
+ if(!target)
+ return
+ switch(action)
+ if(NOTIFY_ATTACK)
+ target.attack_ghost(ghost_user)
+ if(NOTIFY_JUMP)
+ var/turf/gotten_turf = get_turf(target)
+ if(gotten_turf)
+ ghost_user.forceMove(gotten_turf)
+ if(NOTIFY_ORBIT)
+ ghost_user.ManualFollow(target)
+ if(NOTIFY_JOIN_XENO)
+ ghost_user.join_as_alien()
diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm
index 407c64987d88..21a992693aa8 100644
--- a/code/modules/mob/dead/observer/observer.dm
+++ b/code/modules/mob/dead/observer/observer.dm
@@ -256,6 +256,10 @@
A.JumpToCoord(x, y, z)
if(href_list["joinresponseteam"])
JoinResponseTeam()
+ if(href_list["claim_freed"])
+ handle_joining_as_freed_mob(locate(href_list["claim_freed"]))
+ if(href_list["join_xeno"])
+ join_as_alien()
/mob/dead/observer/proc/set_huds_from_prefs()
if(!client || !client.prefs)
@@ -907,19 +911,23 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp
return
var/mob/living/L = freed_mob_choices[choice]
- if(!L || !(WEAKREF(L) in GLOB.freed_mob_list))
+
+ handle_joining_as_freed_mob(L)
+
+/mob/dead/proc/handle_joining_as_freed_mob(mob/living/freed_mob)
+ if(!freed_mob || !(WEAKREF(freed_mob) in GLOB.freed_mob_list))
return
- if(!istype(L))
+ if(!istype(freed_mob))
return
- if(QDELETED(L) || L.client)
- GLOB.freed_mob_list -= WEAKREF(L)
+ if(QDELETED(freed_mob) || freed_mob.client)
+ GLOB.freed_mob_list -= WEAKREF(freed_mob)
to_chat(src, SPAN_WARNING("Something went wrong."))
return
- GLOB.freed_mob_list -= WEAKREF(L)
- M.mind.transfer_to(L, TRUE)
+ GLOB.freed_mob_list -= WEAKREF(freed_mob)
+ mind.transfer_to(freed_mob, TRUE)
/mob/dead/verb/join_as_hellhound()
set category = "Ghost.Join"
diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm
index 3896cd1f9ded..19810893694a 100644
--- a/code/modules/mob/living/carbon/human/death.dm
+++ b/code/modules/mob/living/carbon/human/death.dm
@@ -104,7 +104,7 @@
delayer_armour.can_camo = FALSE //fuck you
to_chat(delayer, SPAN_WARNING("Your [delayer_armour]'s camo system breaks!"))
//tell the ghosts
- announce_dchat("There is only one person left: [last_living_human.real_name].", last_living_human)
+ notify_ghosts(header = "Last Human", message = "There is only one person left: [last_living_human.real_name]!", source = last_living_human, action = NOTIFY_ORBIT)
var/death_message = species.death_message
if(HAS_TRAIT(src, TRAIT_HARDCORE))
diff --git a/code/modules/mob/living/carbon/xenomorph/Embryo.dm b/code/modules/mob/living/carbon/xenomorph/Embryo.dm
index 4ce266f70596..95c0d420b3e5 100644
--- a/code/modules/mob/living/carbon/xenomorph/Embryo.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Embryo.dm
@@ -273,14 +273,8 @@
// Inform observers to grab some popcorn if it isnt nested
if(!HAS_TRAIT(affected_mob, TRAIT_NESTED))
var/area/burst_area = get_area(src)
- if(burst_area)
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("A [new_xeno.hive.prefix]Larva is about to chestburst out of [affected_mob] at \the [burst_area]! [OBSERVER_JMP(observer, affected_mob)]"))
- to_chat(src, SPAN_DEADSAY("A [new_xeno.hive.prefix]Larva is about to chestburst out of [affected_mob] at \the [burst_area]!"))
- else
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("A [new_xeno.hive.prefix]Larva is about to chestburst out of [affected_mob]! [OBSERVER_JMP(observer, affected_mob)]"))
- to_chat(src, SPAN_DEADSAY("A [new_xeno.hive.prefix]Larva is about to chestburst out of [affected_mob]!"))
+ var/area_text = burst_area ? " at [burst_area]" : ""
+ notify_ghosts(header = "Burst Imminent", message = "A [new_xeno.hive.prefix]Larva is about to chestburst out of [affected_mob][area_text]!", source = affected_mob)
stage = 7 // Begin the autoburst countdown
diff --git a/code/modules/mob/living/carbon/xenomorph/Facehuggers.dm b/code/modules/mob/living/carbon/xenomorph/Facehuggers.dm
index bc86ea40361c..5571b122ecaa 100644
--- a/code/modules/mob/living/carbon/xenomorph/Facehuggers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Facehuggers.dm
@@ -284,12 +284,10 @@
var/area/hug_area = get_area(src)
var/name = hugger ? "[hugger]" : "\a [src]"
if(hug_area)
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("[human] has been facehugged by [name] at \the [hug_area] [OBSERVER_JMP(observer, human)]"))
+ notify_ghosts(header = "Hugged", message = "[human] has been hugged by [name] at [hug_area]!", source = human, action = NOTIFY_ORBIT)
to_chat(src, SPAN_DEADSAY("[human] has been facehugged by [name] at \the [hug_area]"))
else
- for(var/mob/dead/observer/observer as anything in GLOB.observer_list)
- to_chat(observer, SPAN_DEADSAY("[human] has been facehugged by [name] [OBSERVER_JMP(observer, human)]"))
+ notify_ghosts(header = "Hugged", message = "[human] has been hugged by [name]!", source = human, action = NOTIFY_ORBIT)
to_chat(src, SPAN_DEADSAY("[human] has been facehugged by [name]"))
if(hug_area)
xeno_message(SPAN_XENOMINORWARNING("You sense that [name] has facehugged a host at \the [hug_area]!"), 1, hivenumber)
diff --git a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
index 32c55ba6fd4c..5c1210f5c845 100644
--- a/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
+++ b/code/modules/mob/living/carbon/xenomorph/Xenomorph.dm
@@ -1091,7 +1091,7 @@
handle_ghost_message()
/mob/living/carbon/xenomorph/proc/handle_ghost_message()
- announce_dchat("[src] ([mutation_type] [caste_type]) has ghosted and their body is up for grabs!", src)
+ notify_ghosts("[src] ([mutation_type] [caste_type]) has ghosted and their body is up for grabs!", source = src)
/mob/living/carbon/xenomorph/larva/handle_ghost_message()
if(locate(/obj/effect/alien/resin/special/pylon/core) in range(2, get_turf(src)))
diff --git a/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm b/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
index 8a7425e2071a..b83b33e2eee5 100644
--- a/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
+++ b/code/modules/mob/living/carbon/xenomorph/castes/Queen.dm
@@ -391,6 +391,7 @@
. = ..()
if(!is_admin_level(z))//so admins can safely spawn Queens in Thunderdome for tests.
xeno_message(SPAN_XENOANNOUNCE("A new Queen has risen to lead the Hive! Rejoice!"),3,hivenumber)
+ notify_ghosts(header = "New Queen", message = "A new Queen has risen.", source = src, action = NOTIFY_ORBIT)
playsound(loc, 'sound/voice/alien_queen_command.ogg', 75, 0)
set_resin_build_order(GLOB.resin_build_order_drone)
for(var/datum/action/xeno_action/action in actions)
diff --git a/code/modules/mob/living/carbon/xenomorph/death.dm b/code/modules/mob/living/carbon/xenomorph/death.dm
index 3a8d344ff68d..d81413b68651 100644
--- a/code/modules/mob/living/carbon/xenomorph/death.dm
+++ b/code/modules/mob/living/carbon/xenomorph/death.dm
@@ -61,6 +61,7 @@
message_alien_candidates(players_with_xeno_pref, dequeued = count)
if(hive && hive.living_xeno_queen == src)
+ notify_ghosts(header = "Queen Death", message = "The Queen has been slain!", source = src, action = NOTIFY_ORBIT)
xeno_message(SPAN_XENOANNOUNCE("A sudden tremor ripples through the hive... the Queen has been slain! Vengeance!"),3, hivenumber)
hive.slashing_allowed = XENO_SLASH_ALLOWED
hive.set_living_xeno_queen(null)
@@ -129,7 +130,7 @@
// Tell the xeno she is the last one.
if(X.client)
to_chat(X, SPAN_XENOANNOUNCE("Your carapace rattles with dread. You are all that remains of the hive!"))
- announce_dchat("There is only one Xenomorph left: [X.name].", X)
+ notify_ghosts(header = "Last Xenomorph", message = "There is only one Xenomorph left: [X.name].", source = X, action = NOTIFY_ORBIT)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_XENO_DEATH, src, gibbed)
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index b469052104fd..3e765e167ec1 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -271,6 +271,9 @@
/// User is thinking in character. Used to revert to thinking state after stop_typing
var/thinking_IC = FALSE
+ // contains /atom/movable/screen/alert only
+ var/list/alerts = list()
+
/mob/vv_get_dropdown()
. = ..()
VV_DROPDOWN_OPTION(VV_HK_EXPLODE, "Trigger Explosion")
diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm
index 32a3ca51d456..6eb32501512f 100644
--- a/code/modules/mob/mob_helpers.dm
+++ b/code/modules/mob/mob_helpers.dm
@@ -529,8 +529,68 @@ var/global/list/limb_types_by_name = list(
/mob/proc/get_paygrade()
return
+
+/proc/notify_ghosts(message, ghost_sound = null, enter_link = null, enter_text = null, atom/source = null, mutable_appearance/alert_overlay = null, action = NOTIFY_JUMP, flashwindow = FALSE, ignore_mapload = TRUE, ignore_key, header = null, notify_volume = 100, extra_large = FALSE) //Easy notification of ghosts.
+ if(ignore_mapload && SSatoms.initialized != INITIALIZATION_INNEW_REGULAR) //don't notify for objects created during a map load
+ return
+ for(var/mob/dead/observer/ghost as anything in GLOB.observer_list)
+ if(!ghost.client)
+ continue
+ ghost.notify_ghost(message, ghost_sound, enter_link, enter_text, source, alert_overlay, action, flashwindow, ignore_mapload, ignore_key, header, notify_volume, extra_large)
+
+/mob/dead/observer/proc/notify_ghost(message, ghost_sound, enter_link, enter_text, atom/source, mutable_appearance/alert_overlay, action = NOTIFY_JUMP, flashwindow = FALSE, ignore_mapload = TRUE, ignore_key, header, notify_volume = 100, extra_large = FALSE) //Easy notification of a single ghosts.
+ if(ignore_mapload && SSatoms.initialized != INITIALIZATION_INNEW_REGULAR) //don't notify for objects created during a map load
+ return
+ if(!client)
+ return
+ var/track_link
+ if (source && action == NOTIFY_ORBIT)
+ track_link = " (Follow)"
+ if (source && action == NOTIFY_JUMP)
+ var/turf/T = get_turf(source)
+ track_link = " (Jump)"
+ var/full_enter_link
+ if (enter_link)
+ full_enter_link = "[(enter_text) ? "[enter_text]" : "(Claim)"]"
+ to_chat(src, "[(extra_large) ? "