diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm index 3184a169fc..c0f883707a 100644 --- a/code/modules/antagonists/_common/antag_datum.dm +++ b/code/modules/antagonists/_common/antag_datum.dm @@ -23,6 +23,7 @@ GLOBAL_LIST_EMPTY(antagonists) var/show_name_in_check_antagonists = FALSE //Will append antagonist name in admin listings - use for categories that share more than one antag type var/list/blacklisted_quirks = list(/datum/quirk/nonviolent,/datum/quirk/mute) // Quirks that will be removed upon gaining this antag. Pacifist and mute are default. var/threat = 0 // Amount of threat this antag poses, for dynamic mode + var/show_to_ghosts = FALSE // Should this antagonist be shown as antag to ghosts? Shouldn't be used for stealthy antagonists like traitors var/list/skill_modifiers diff --git a/code/modules/antagonists/abductor/abductor.dm b/code/modules/antagonists/abductor/abductor.dm index 9132288415..1a1db834c2 100644 --- a/code/modules/antagonists/abductor/abductor.dm +++ b/code/modules/antagonists/abductor/abductor.dm @@ -7,6 +7,7 @@ job_rank = ROLE_ABDUCTOR show_in_antagpanel = FALSE //should only show subtypes threat = 5 + show_to_ghosts = TRUE var/datum/team/abductor_team/team var/sub_role var/outfit @@ -32,6 +33,10 @@ show_in_antagpanel = TRUE skill_modifiers = list(/datum/skill_modifier/job/affinity/surgery) +/datum/antagonist/abductor/scientist/onemanteam + name = "Abductor Solo" + outfit = /datum/outfit/abductor/scientist/onemanteam + /datum/antagonist/abductor/create_team(datum/team/abductor_team/new_team) if(!new_team) return diff --git a/code/modules/antagonists/blob/blob.dm b/code/modules/antagonists/blob/blob.dm index 1b076c9302..9a82bb546b 100644 --- a/code/modules/antagonists/blob/blob.dm +++ b/code/modules/antagonists/blob/blob.dm @@ -2,6 +2,7 @@ name = "Blob" roundend_category = "blobs" antagpanel_category = "Blob" + show_to_ghosts = TRUE job_rank = ROLE_BLOB threat = 20 var/datum/action/innate/blobpop/pop_action diff --git a/code/modules/antagonists/devil/devil.dm b/code/modules/antagonists/devil/devil.dm index c12259778e..3b6dc68986 100644 --- a/code/modules/antagonists/devil/devil.dm +++ b/code/modules/antagonists/devil/devil.dm @@ -92,6 +92,7 @@ GLOBAL_LIST_INIT(devil_suffix, list(" the Red", " the Soulless", " the Master", //Don't delete upon mind destruction, otherwise soul re-selling will break. delete_on_mind_deletion = FALSE threat = 5 + show_to_ghosts = TRUE var/obligation var/ban var/bane diff --git a/code/modules/antagonists/disease/disease_datum.dm b/code/modules/antagonists/disease/disease_datum.dm index 7de0330ad6..b8e906064c 100644 --- a/code/modules/antagonists/disease/disease_datum.dm +++ b/code/modules/antagonists/disease/disease_datum.dm @@ -2,6 +2,7 @@ name = "Sentient Disease" roundend_category = "diseases" antagpanel_category = "Disease" + show_to_ghosts = TRUE var/disease_name = "" /datum/antagonist/disease/on_gain() diff --git a/code/modules/antagonists/ert/ert.dm b/code/modules/antagonists/ert/ert.dm index 1d773627c7..295616d052 100644 --- a/code/modules/antagonists/ert/ert.dm +++ b/code/modules/antagonists/ert/ert.dm @@ -12,6 +12,7 @@ var/list/name_source threat = -5 show_in_antagpanel = FALSE + show_to_ghosts = TRUE antag_moodlet = /datum/mood_event/focused /datum/antagonist/ert/on_gain() diff --git a/code/modules/antagonists/monkey/monkey.dm b/code/modules/antagonists/monkey/monkey.dm index ebb39c814e..971532958f 100644 --- a/code/modules/antagonists/monkey/monkey.dm +++ b/code/modules/antagonists/monkey/monkey.dm @@ -9,6 +9,7 @@ roundend_category = "monkeys" antagpanel_category = "Monkey" threat = 3 + show_to_ghosts = TRUE var/datum/team/monkey/monkey_team var/monkey_only = TRUE diff --git a/code/modules/antagonists/nightmare/nightmare.dm b/code/modules/antagonists/nightmare/nightmare.dm index 837b6e4216..f5b10de5c2 100644 --- a/code/modules/antagonists/nightmare/nightmare.dm +++ b/code/modules/antagonists/nightmare/nightmare.dm @@ -3,3 +3,4 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE threat = 5 + show_to_ghosts = TRUE diff --git a/code/modules/antagonists/ninja/ninja.dm b/code/modules/antagonists/ninja/ninja.dm index 2615822dd8..414f7dd6b0 100644 --- a/code/modules/antagonists/ninja/ninja.dm +++ b/code/modules/antagonists/ninja/ninja.dm @@ -3,6 +3,7 @@ antagpanel_category = "Ninja" job_rank = ROLE_NINJA show_name_in_check_antagonists = TRUE + show_to_ghosts = TRUE antag_moodlet = /datum/mood_event/focused threat = 8 var/helping_station = FALSE diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm index 454cde6d72..652b19a8e7 100644 --- a/code/modules/antagonists/nukeop/nukeop.dm +++ b/code/modules/antagonists/nukeop/nukeop.dm @@ -6,6 +6,7 @@ antag_moodlet = /datum/mood_event/focused threat = 10 skill_modifiers = list(/datum/skill_modifier/job/level/wiring) + show_to_ghosts = TRUE var/datum/team/nuclear/nuke_team var/always_new_team = FALSE //If not assigned a team by default ops will try to join existing ones, set this to TRUE to always create new team. var/send_to_spawnpoint = TRUE //Should the user be moved to default spawnpoint. diff --git a/code/modules/antagonists/official/official.dm b/code/modules/antagonists/official/official.dm index 1d340253c4..1ec64cb2b6 100644 --- a/code/modules/antagonists/official/official.dm +++ b/code/modules/antagonists/official/official.dm @@ -4,6 +4,7 @@ show_in_antagpanel = FALSE var/datum/objective/mission var/datum/team/ert/ert_team + show_to_ghosts = TRUE /datum/antagonist/official/greet() to_chat(owner, "You are a CentCom Official.") diff --git a/code/modules/antagonists/pirate/pirate.dm b/code/modules/antagonists/pirate/pirate.dm index 01f3c6068e..e6d350064d 100644 --- a/code/modules/antagonists/pirate/pirate.dm +++ b/code/modules/antagonists/pirate/pirate.dm @@ -4,6 +4,7 @@ roundend_category = "space pirates" antagpanel_category = "Pirate" threat = 5 + show_to_ghosts = TRUE var/datum/team/pirate/crew /datum/antagonist/pirate/greet() diff --git a/code/modules/antagonists/revenant/revenant_antag.dm b/code/modules/antagonists/revenant/revenant_antag.dm index 46c1176533..c93291797a 100644 --- a/code/modules/antagonists/revenant/revenant_antag.dm +++ b/code/modules/antagonists/revenant/revenant_antag.dm @@ -3,6 +3,7 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE threat = 5 + show_to_ghosts = TRUE /datum/antagonist/revenant/greet() owner.announce_objectives() diff --git a/code/modules/antagonists/santa/santa.dm b/code/modules/antagonists/santa/santa.dm index f58a21ba42..ff7dae98f6 100644 --- a/code/modules/antagonists/santa/santa.dm +++ b/code/modules/antagonists/santa/santa.dm @@ -1,6 +1,8 @@ /datum/antagonist/santa name = "Santa" show_in_antagpanel = FALSE + show_name_in_check_antagonists = TRUE + show_to_ghosts = TRUE /datum/antagonist/santa/on_gain() . = ..() diff --git a/code/modules/antagonists/slaughter/slaughter_antag.dm b/code/modules/antagonists/slaughter/slaughter_antag.dm index 04f7167fa5..87db9772b7 100644 --- a/code/modules/antagonists/slaughter/slaughter_antag.dm +++ b/code/modules/antagonists/slaughter/slaughter_antag.dm @@ -6,6 +6,7 @@ threat = 10 job_rank = ROLE_ALIEN show_in_antagpanel = FALSE + show_to_ghosts = TRUE /datum/antagonist/slaughter/on_gain() forge_objectives() diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 70adafd3fb..7263793f7f 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -13,6 +13,7 @@ var/move_to_lair = TRUE var/outfit_type = /datum/outfit/wizard var/wiz_age = WIZARD_AGE_MIN /* Wizards by nature cannot be too young. */ + show_to_ghosts = TRUE /datum/antagonist/wizard/on_gain() register() diff --git a/code/modules/antagonists/xeno/xeno.dm b/code/modules/antagonists/xeno/xeno.dm index 7c4c5351df..2cc8e34b99 100644 --- a/code/modules/antagonists/xeno/xeno.dm +++ b/code/modules/antagonists/xeno/xeno.dm @@ -12,6 +12,7 @@ name = "Xenomorph" job_rank = ROLE_ALIEN show_in_antagpanel = FALSE + show_to_ghosts = TRUE var/datum/team/xeno/xeno_team threat = 3 diff --git a/code/modules/asset_cache/asset_list_items.dm b/code/modules/asset_cache/asset_list_items.dm index 50eedcad93..d4289b9b31 100644 --- a/code/modules/asset_cache/asset_list_items.dm +++ b/code/modules/asset_cache/asset_list_items.dm @@ -369,9 +369,12 @@ "dna_extra.gif" = 'html/dna_extra.gif' ) -/* /datum/asset/simple/orbit assets = list( "ghost.png" = 'html/ghost.png' ) + + assets = list( + "ghost.png" = 'html/ghost.png' + ) */ diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 397af1b9d0..622a1a2221 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -54,6 +54,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) // Used for displaying in ghost chat, without changing the actual name // of the mob var/deadchat_name + var/datum/orbit_menu/orbit_menu var/datum/spawners_menu/spawners_menu /mob/dead/observer/Initialize(mapload, mob/body) @@ -161,6 +162,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) updateallghostimages() + QDEL_NULL(orbit_menu) QDEL_NULL(spawners_menu) return ..() @@ -490,10 +492,10 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp set name = "Orbit" // "Haunt" set desc = "Follow and orbit a mob." - var/list/mobs = getpois(skip_mindless=1) - var/input = input("Please, select a mob!", "Haunt", null, null) as null|anything in mobs - var/mob/target = mobs[input] - ManualFollow(target) + if(!orbit_menu) + orbit_menu = new(src) + + orbit_menu.ui_interact(src) // This is the ghost's follow verb with an argument /mob/dead/observer/proc/ManualFollow(atom/movable/target) diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm new file mode 100644 index 0000000000..137de92db8 --- /dev/null +++ b/code/modules/mob/dead/observer/orbit.dm @@ -0,0 +1,78 @@ +/datum/orbit_menu + var/mob/dead/observer/owner + +/datum/orbit_menu/New(mob/dead/observer/new_owner) + if(!istype(new_owner)) + qdel(src) + owner = new_owner + +/datum/orbit_menu/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.observer_state) + if (!ui) + ui = new(user, src, ui_key, "Orbit", "Orbit", 350, 700, master_ui, state) + ui.open() + +/datum/orbit_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if (..()) + return + + if (action == "orbit") + var/list/pois = getpois(skip_mindless = 1) + var/atom/movable/poi = pois[params["name"]] + if (poi != null) + owner.ManualFollow(poi) + ui.close() + +/datum/orbit_menu/ui_data(mob/user) + var/list/data = list() + + var/list/alive = list() + var/list/antagonists = list() + var/list/dead = list() + var/list/ghosts = list() + var/list/misc = list() + var/list/npcs = list() + + var/list/pois = getpois(skip_mindless = 1) + for (var/name in pois) + var/list/serialized = list() + serialized["name"] = name + + var/poi = pois[name] + + var/mob/M = poi + if (istype(M)) + if (isobserver(M)) + ghosts += list(serialized) + else if (M.stat == DEAD) + dead += list(serialized) + else if (M.mind == null) + npcs += list(serialized) + else + var/number_of_orbiters = M.orbiters?.orbiters?.len + if (number_of_orbiters) + serialized["orbiters"] = number_of_orbiters + + var/datum/mind/mind = M.mind + var/was_antagonist = FALSE + + for (var/_A in mind.antag_datums) + var/datum/antagonist/A = _A + if (A.show_to_ghosts) + was_antagonist = TRUE + serialized["antag"] = A.name + antagonists += list(serialized) + break + + if (!was_antagonist) + alive += list(serialized) + else + misc += list(serialized) + + data["alive"] = alive + data["antagonists"] = antagonists + data["dead"] = dead + data["ghosts"] = ghosts + data["misc"] = misc + data["npcs"] = npcs + + return data diff --git a/html/ghost.png b/html/ghost.png new file mode 100644 index 0000000000..e4b426ca47 Binary files /dev/null and b/html/ghost.png differ diff --git a/tgstation.dme b/tgstation.dme index a4f0fcf779..d80b9ccd09 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2319,6 +2319,8 @@ #include "code\modules\mob\dead\observer\observer.dm" #include "code\modules\mob\dead\observer\observer_movement.dm" #include "code\modules\mob\dead\observer\say.dm" +#include "code\modules\mob\dead\observer\observer_say.dm" +#include "code\modules\mob\dead\observer\orbit.dm" #include "code\modules\mob\living\blood.dm" #include "code\modules\mob\living\bloodcrawl.dm" #include "code\modules\mob\living\damage_procs.dm" diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js new file mode 100644 index 0000000000..ec81293a80 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit.js @@ -0,0 +1,188 @@ +import { createSearch } from 'common/string'; +import { Box, Button, Input, Section } from '../components'; +import { Window } from '../layouts'; +import { useBackend, useLocalState } from '../backend'; + +const PATTERN_DESCRIPTOR = / \[(?:ghost|dead)\]$/; +const PATTERN_NUMBER = / \(([0-9]+)\)$/; + +const searchFor = searchText => createSearch(searchText, thing => thing.name); + +const compareString = (a, b) => a < b ? -1 : a > b; + +const compareNumberedText = (a, b) => { + const aName = a.name; + const bName = b.name; + + // Check if aName and bName are the same except for a number at the end + // e.g. Medibot (2) and Medibot (3) + const aNumberMatch = aName.match(PATTERN_NUMBER); + const bNumberMatch = bName.match(PATTERN_NUMBER); + + if (aNumberMatch + && bNumberMatch + && aName.replace(PATTERN_NUMBER, "") === bName.replace(PATTERN_NUMBER, "") + ) { + const aNumber = parseInt(aNumberMatch[1], 10); + const bNumber = parseInt(bNumberMatch[1], 10); + + return aNumber - bNumber; + } + + return compareString(aName, bName); +}; + +const BasicSection = (props, context) => { + const { act } = useBackend(context); + const { searchText, source, title } = props; + const things = source.filter(searchFor(searchText)); + things.sort(compareNumberedText); + return source.length > 0 && ( +
+ {things.map(thing => ( +
+ ); +}; + +const OrbitedButton = (props, context) => { + const { act } = useBackend(context); + const { color, thing } = props; + + return ( + + ); +}; + +export const Orbit = (props, context) => { + const { act, data } = useBackend(context); + const { + alive, + antagonists, + dead, + ghosts, + misc, + npcs, + } = data; + + const [searchText, setSearchText] = useLocalState(context, "searchText", ""); + + const collatedAntagonists = {}; + for (const antagonist of antagonists) { + if (collatedAntagonists[antagonist.antag] === undefined) { + collatedAntagonists[antagonist.antag] = []; + } + collatedAntagonists[antagonist.antag].push(antagonist); + } + + const sortedAntagonists = Object.entries(collatedAntagonists); + sortedAntagonists.sort((a, b) => { + return compareString(a[0], b[0]); + }); + + const orbitMostRelevant = searchText => { + for (const source of [ + sortedAntagonists.map(([_, antags]) => antags), + alive, ghosts, dead, npcs, misc, + ]) { + const member = source + .filter(searchFor(searchText)) + .sort(compareNumberedText)[0]; + if (member !== undefined) { + act("orbit", { name: member.name }); + break; + } + } + }; + + return ( + + +
+ setSearchText(value)} + onEnter={(_, value) => orbitMostRelevant(value)} /> +
+ + {antagonists.length > 0 && ( +
+ {sortedAntagonists.map(([name, antags]) => ( +
+ {antags + .filter(searchFor(searchText)) + .sort(compareNumberedText) + .map(antag => ( + + ))} +
+ ))} +
+ )} + +
+ {alive + .filter(searchFor(searchText)) + .sort(compareNumberedText) + .map(thing => ( + + ))} +
+ + + + + + + + +
+
+ ); +};