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) ? "

" : ""][SPAN_DEADSAY("[message][(enter_link) ? " [full_enter_link]" : ""][track_link]")][(extra_large) ? "

" : ""]") + if(ghost_sound) + SEND_SOUND(src, sound(ghost_sound, volume = notify_volume, channel = SOUND_CHANNEL_NOTIFY)) + if(flashwindow) + window_flash(client) + + if(!source) + return + + var/atom/movable/screen/alert/notify_action/screen_alert = throw_alert("[REF(source)]_notify_action", /atom/movable/screen/alert/notify_action) + if(!screen_alert) + return + if (header) + screen_alert.name = header + screen_alert.desc = message + screen_alert.action = action + screen_alert.target = source + if(!alert_overlay) + alert_overlay = new(source) + var/icon/source_icon = icon(source.icon) + var/iheight = source_icon.Height() + var/iwidth = source_icon.Width() + var/higher_power = (iheight > iwidth) ? iheight : iwidth + if(higher_power > 32) + var/diff = 32 / higher_power + alert_overlay.transform = alert_overlay.transform.Scale(diff, diff) + if(higher_power > 48) + alert_overlay.pixel_y = -(iheight / 2) * diff + alert_overlay.pixel_x = -(iwidth / 2) * diff + + + alert_overlay.layer = FLOAT_LAYER + alert_overlay.plane = FLOAT_PLANE + + screen_alert.overlays += alert_overlay + /mob/proc/reset_lighting_alpha() SIGNAL_HANDLER lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE sync_lighting_plane_alpha() + diff --git a/code/modules/shuttle/computers/dropship_computer.dm b/code/modules/shuttle/computers/dropship_computer.dm index 16f96ce3017c..7e8dc34f6892 100644 --- a/code/modules/shuttle/computers/dropship_computer.dm +++ b/code/modules/shuttle/computers/dropship_computer.dm @@ -220,7 +220,7 @@ dropship.control_doors("unlock", "all", TRUE) dropship_control_lost = TRUE door_control_cooldown = addtimer(CALLBACK(src, PROC_REF(remove_door_lock)), SHUTTLE_LOCK_COOLDOWN, TIMER_STOPPABLE) - announce_dchat("[xeno] has locked \the [dropship]", src) + notify_ghosts(header = "Dropship Locked", message = "[xeno] has locked [dropship]!", source = xeno, action = NOTIFY_ORBIT) if(almayer_orbital_cannon) almayer_orbital_cannon.is_disabled = TRUE diff --git a/code/modules/shuttle/dropship_hijack.dm b/code/modules/shuttle/dropship_hijack.dm index c06abb6d3f22..7796ed0510c8 100644 --- a/code/modules/shuttle/dropship_hijack.dm +++ b/code/modules/shuttle/dropship_hijack.dm @@ -151,7 +151,7 @@ marine_announcement("DROPSHIP ON COLLISION COURSE. CRASH IMMINENT." , "EMERGENCY", 'sound/AI/dropship_emergency.ogg', logging = ARES_LOG_SECURITY) - announce_dchat("The dropship is about to impact [get_area_name(crash_site)]", crash_site) + notify_ghosts(header = "Dropship Collision", message = "The dropship is about to impact [get_area_name(crash_site)]!", source = crash_site, extra_large = TRUE) final_announcement = TRUE playsound_area(get_area(crash_site), 'sound/effects/engine_landing.ogg', 100) diff --git a/icons/mob/screen_alert.dmi b/icons/mob/screen_alert.dmi new file mode 100644 index 000000000000..af61a47aa885 Binary files /dev/null and b/icons/mob/screen_alert.dmi differ