diff --git a/baystation12.dme b/baystation12.dme index 9f75b7451e2..4154609d277 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -172,6 +172,7 @@ #include "code\_onclick\hud\animal.dm" #include "code\_onclick\hud\chorus.dm" #include "code\_onclick\hud\fullscreen.dm" +#include "code\_onclick\hud\ghost.dm" #include "code\_onclick\hud\global_hud.dm" #include "code\_onclick\hud\global_hud_inf.dm" #include "code\_onclick\hud\gun_mode.dm" @@ -2464,6 +2465,7 @@ #include "code\modules\mob\observer\ghost\ghost.dm" #include "code\modules\mob\observer\ghost\login.dm" #include "code\modules\mob\observer\ghost\logout.dm" +#include "code\modules\mob\observer\ghost\orbit.dm" #include "code\modules\mob\observer\ghost\say.dm" #include "code\modules\mob\observer\virtual\_constants.dm" #include "code\modules\mob\observer\virtual\base.dm" diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm index 543f420e6ea..167565f25ad 100644 --- a/code/_onclick/hud/_defines.dm +++ b/code/_onclick/hud/_defines.dm @@ -133,3 +133,11 @@ #define ui_pai_light "NORTH,WEST+3:6" #define ui_pai_rest "NORTH,WEST+4:6" +// Ghosts +#define ui_ghost_toggle_darkness "SOUTH:6,CENTER-3:16" +#define ui_ghost_jumptomob "SOUTH:6,CENTER-2:16" +#define ui_ghost_orbit "SOUTH:6,CENTER-1:16" +#define ui_ghost_reenter_corpse "SOUTH:6,CENTER:16" +#define ui_ghost_teleport "SOUTH:6,CENTER+1:16" +#define ui_ghost_mafia "SOUTH:6,CENTER+2:16" +#define ui_ghost_spawners_menu "SOUTH:6,CENTER-4:16" diff --git a/code/_onclick/hud/ghost.dm b/code/_onclick/hud/ghost.dm new file mode 100644 index 00000000000..d487d6bf772 --- /dev/null +++ b/code/_onclick/hud/ghost.dm @@ -0,0 +1,51 @@ +/obj/screen/ghost + icon = 'icons/hud/screen_ghost.dmi' + +/obj/screen/ghost/MouseExited(location, control, params) + . = ..() + flick(icon_state + "_anim", src) + +/obj/screen/ghost/jumptomob + name = "Jump to mob" + icon_state = "jumptomob" + screen_loc = ui_ghost_jumptomob + +/obj/screen/ghost/jumptomob/Click() + var/mob/observer/ghost/G = usr + G.jumptomob() + +/obj/screen/ghost/orbit + name = "Orbit" + icon_state = "orbit" + screen_loc = ui_ghost_orbit + +/obj/screen/ghost/orbit/Click() + var/mob/observer/ghost/G = usr + G.follow() + +/obj/screen/ghost/reenter_corpse + name = "Reenter corpse" + icon_state = "reenter_corpse" + screen_loc = ui_ghost_reenter_corpse + +/obj/screen/ghost/reenter_corpse/Click() + var/mob/observer/ghost/G = usr + G.reenter_corpse() + +/obj/screen/ghost/teleport + name = "Teleport" + icon_state = "teleport" + screen_loc = ui_ghost_teleport + +/obj/screen/ghost/teleport/Click() + var/mob/observer/ghost/G = usr + G.dead_tele() + +/obj/screen/ghost/toggle_darkness + name = "Toggle Darkness" + icon_state = "toggle_darkness" + screen_loc = ui_ghost_toggle_darkness + +/obj/screen/ghost/toggle_darkness/Click() + var/mob/observer/ghost/G = usr + G.toggle_darkness() diff --git a/code/modules/mob/new_player/new_player.dm b/code/modules/mob/new_player/new_player.dm index 047510e4eaf..2d353567ca2 100644 --- a/code/modules/mob/new_player/new_player.dm +++ b/code/modules/mob/new_player/new_player.dm @@ -191,6 +191,7 @@ if(!client.holder && !config.antag_hud_allowed) // For new ghosts we remove the verb from even showing up if it's not allowed. observer.verbs -= /mob/observer/ghost/verb/toggle_antagHUD // Poor guys, don't know what they are missing! observer.key = key + observer.add_ghost_buttons() qdel(src) return 1 diff --git a/code/modules/mob/observer/following.dm b/code/modules/mob/observer/following.dm index 6ef4ae13b3c..bf53b5bc670 100644 --- a/code/modules/mob/observer/following.dm +++ b/code/modules/mob/observer/following.dm @@ -14,12 +14,13 @@ following = null /mob/observer/proc/start_following(var/atom/a) - stop_following() - following = a - GLOB.destroyed_event.register(a, src, .proc/stop_following) - GLOB.moved_event.register(a, src, .proc/keep_following) - GLOB.dir_set_event.register(a, src, /atom/proc/recursive_dir_set) - keep_following(new_loc = get_turf(following)) + if(!istype(a, /obj/screen)) + stop_following() + following = a + GLOB.destroyed_event.register(a, src, .proc/stop_following) + GLOB.moved_event.register(a, src, .proc/keep_following) + GLOB.dir_set_event.register(a, src, /atom/proc/recursive_dir_set) + keep_following(new_loc = get_turf(following)) /mob/observer/proc/keep_following(var/atom/movable/moving_instance, var/atom/old_loc, var/atom/new_loc) - forceMove(get_turf(new_loc)) \ No newline at end of file + forceMove(get_turf(new_loc)) diff --git a/code/modules/mob/observer/ghost/ghost.dm b/code/modules/mob/observer/ghost/ghost.dm index 5920ee86e5d..fd53203f261 100644 --- a/code/modules/mob/observer/ghost/ghost.dm +++ b/code/modules/mob/observer/ghost/ghost.dm @@ -32,6 +32,8 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images var/obj/item/device/multitool/ghost_multitool var/list/hud_images // A list of hud images + var/thearea + /mob/observer/ghost/New(mob/body) see_in_dark = 100 verbs += /mob/proc/toggle_antag_pool @@ -71,7 +73,6 @@ var/global/list/image/ghost_sightless_images = list() //this is a list of images ghost_multitool = new(src) GLOB.ghost_mob_list += src - ..() /mob/observer/ghost/Destroy() @@ -108,7 +109,6 @@ Works together with spawning an observer, noted above. ..() if(!loc) return if(!client) return 0 - handle_hud_glasses() if(antagHUD) @@ -150,6 +150,7 @@ Works together with spawning an observer, noted above. ghost.key = key if(ghost.client && !ghost.client.holder && !config.antag_hud_allowed) // For new ghosts we remove the verb from even showing up if it's not allowed. ghost.verbs -= /mob/observer/ghost/verb/toggle_antagHUD // Poor guys, don't know what they are missing! + ghost.add_ghost_buttons() return ghost /mob/observer/ghostize() // Do not create ghosts of ghosts. @@ -233,6 +234,16 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp announce_ghost_joinleave(mind, 0, "They now occupy their body again.") return 1 +/mob/observer/ghost/proc/jumptomob() + var/mob/M = input(usr, "Pick a mob", "Pick a mob") as null|anything in SSmobs.mob_list + log_and_message_admins("jumped to [key_name(M)]") + var/turf/T = get_turf(M) + if(T && isturf(T)) + jumpTo(T) + else + to_chat(usr, "This mob is not located in the game world.") + + /mob/observer/ghost/verb/toggle_medHUD() set category = "Ghost" set name = "Toggle MedicHUD" @@ -273,16 +284,17 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp M.antagHUD = 1 to_chat(src, SPAN_NOTICE("AntagHUD Enabled")) -/mob/observer/ghost/verb/dead_tele(A in area_repository.get_areas_by_z_level()) +/mob/observer/ghost/verb/dead_tele() set category = "Ghost" set name = "Teleport" set desc= "Teleport to a location" + var/A = input(usr, "Pick an area.", "Pick an area") as num|anything in area_repository.get_areas_by_z_level() var/area/thearea = area_repository.get_areas_by_z_level()[A] + if(!thearea) to_chat(src, "No area available.") return - var/list/area_turfs = get_area_turfs(thearea, shall_check_if_holy() ? list(/proc/is_not_holy_turf) : list()) if(!area_turfs.len) to_chat(src, "<span class='warning'>This area has been entirely made into sacred grounds, you cannot enter it while you are in this plane of existence!</span>") @@ -300,13 +312,13 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp ghost_to_turf(T) else to_chat(src, "<span class='warning'>Invalid coordinates.</span>") -/mob/observer/ghost/verb/follow(var/datum/follow_holder/fh in get_follow_targets()) + +/mob/observer/ghost/verb/follow() set category = "Ghost" set name = "Follow" set desc = "Follow and haunt a mob." - if(!fh.show_entry()) return - start_following(fh.followed_instance) + GLOB.orbit_menu.show(src) /mob/observer/ghost/proc/ghost_to_turf(var/turf/target_turf) if(check_is_holy_turf(target_turf)) @@ -326,7 +338,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp verbs -= /mob/observer/ghost/verb/scan_target ..() -/mob/observer/ghost/keep_following(var/atom/movable/am, var/old_loc, var/new_loc) +/mob/observer/ghost/keep_following(var/obj/am, var/old_loc, var/new_loc) var/turf/T = get_turf(new_loc) if(check_is_holy_turf(T)) to_chat(src, "<span class='warning'>You cannot follow something standing on holy grounds!</span>") @@ -623,3 +635,15 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp M.respawned_time = world.time M.key = key log_and_message_admins("has respawned.", M) + +/mob/observer/ghost/proc/add_ghost_buttons() + var/jumptomob = new /obj/screen/ghost/jumptomob() + var/orbit = new /obj/screen/ghost/orbit() + var/reenter_corpse = new /obj/screen/ghost/reenter_corpse() + var/teleport = new /obj/screen/ghost/teleport() + var/toggle_darkness = new /obj/screen/ghost/toggle_darkness() + client.screen.Add(jumptomob) + client.screen.Add(orbit) + client.screen.Add(reenter_corpse) + client.screen.Add(teleport) + client.screen.Add(toggle_darkness) diff --git a/code/modules/mob/observer/ghost/orbit.dm b/code/modules/mob/observer/ghost/orbit.dm new file mode 100644 index 00000000000..0c211df84d6 --- /dev/null +++ b/code/modules/mob/observer/ghost/orbit.dm @@ -0,0 +1,75 @@ +GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new) + +/datum/orbit_menu + +/datum/orbit_menu/tgui_state(mob/user) + return GLOB.tgui_observer_state + +/datum/orbit_menu/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "Orbit", "Orbit") + ui.open() + +/datum/orbit_menu/tgui_act(action, list/params) + . = ..() + + if(.) + return + + switch(action) + if("orbit") + var/datum/follow_holder/fh = locate(params["ref"]) in get_follow_targets() + var/atom/movable/a = fh.followed_instance + var/mob/observer/ghost/G = usr + if(a != usr) + G.start_following(a) + return TRUE + if("refresh") + update_tgui_static_data() + return TRUE + +/datum/orbit_menu/tgui_static_data(mob/user) + var/list/data = list() + data["misc"] = list() + data["ghosts"] = list() + data["dead"] = list() + data["npcs"] = list() + data["alive"] = list() + data["antagonists"] = list() + for(var/datum/follow_holder/fh in get_follow_targets()) + var/atom/movable/fi = fh.followed_instance + var/list/serialized = list() + serialized["name"] = fi.name + serialized["ref"] = "\ref[fh]" + + if(!istype(fi, /mob)) + data["misc"] += list(serialized) + continue + var/mob/M = fi + if(isobserver(M)) + data["ghosts"] += list(serialized) + continue + + if(M.stat == DEAD) + data["dead"] += list(serialized) + continue + + if(M.mind == null) + data["npcs"] += list(serialized) + continue + + data["alive"] += list(serialized) + + var/mob/observer/ghost/O = user + if(O.antagHUD && M.get_antag_info()) + var/antag_serialized = serialized.Copy() + for(var/antag_category in M.get_antag_info()) + antag_serialized["antag"] += list(antag_category) + data["antagonists"] += list(antag_serialized) + + return data + +/// Shows the UI to the specified user. +/datum/orbit_menu/proc/show(mob/user) + tgui_interact(user) diff --git a/icons/hud/screen_ghost.dmi b/icons/hud/screen_ghost.dmi new file mode 100644 index 00000000000..52d72c731fc Binary files /dev/null and b/icons/hud/screen_ghost.dmi differ diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js new file mode 100644 index 00000000000..d4c4f603543 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Orbit.js @@ -0,0 +1,181 @@ +import { createSearch } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Button, Divider, Flex, Icon, Input, Section } from '../components'; +import { Window } from '../layouts'; + +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; + + if (!aName || !bName) { + return 0; + } + + // 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 && ( + <Section title={`${title} - (${source.length})`}> + {things.map((thing) => ( + <Button + key={thing.name} + content={thing.name} + onClick={() => + act('orbit', { + ref: thing.ref, + }) + } + /> + ))} + </Section> + ) + ); +}; + +const OrbitedButton = (props, context) => { + const { act } = useBackend(context); + const { color, thing } = props; + + return ( + <Button + color={color} + onClick={() => + act('orbit', { + ref: thing.ref, + }) + }> + {thing.name} + </Button> + ); +}; + +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) { + for (const anta of antagonist.antag) { + if (collatedAntagonists[anta] === undefined) { + collatedAntagonists[anta] = []; + } + collatedAntagonists[anta].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', { ref: member.ref }); + break; + } + } + }; + + return ( + <Window width={800} height={600}> + <Window.Content scrollable> + <Section> + <Flex> + <Flex.Item> + <Icon name="search" mr={1} /> + </Flex.Item> + <Flex.Item grow={1}> + <Input + placeholder="Search..." + autoFocus + fluid + value={searchText} + onInput={(_, value) => setSearchText(value)} + onEnter={(_, value) => orbitMostRelevant(value)} + /> + </Flex.Item> + <Flex.Item> + <Divider vertical /> + </Flex.Item> + <Flex.Item> + <Button + inline + color="transparent" + tooltip="Refresh" + tooltipPosition="bottom-start" + icon="sync-alt" + onClick={() => act('refresh')} + /> + </Flex.Item> + </Flex> + </Section> + {antagonists.length > 0 && ( + <Section title="Antagonists"> + {sortedAntagonists.map(([name, antags]) => ( + <Section key={name} title={name} level={2}> + {antags + .filter(searchFor(searchText)) + .sort(compareNumberedText) + .map((antag) => ( + <OrbitedButton key={antag.name} color="bad" thing={antag} /> + ))} + </Section> + ))} + </Section> + )} + + <Section title={`Alive - (${alive.length})`}> + {alive + .filter(searchFor(searchText)) + .sort(compareNumberedText) + .map((thing) => ( + <OrbitedButton key={thing.name} color="good" thing={thing} /> + ))} + </Section> + + <Section title={`Ghosts - (${ghosts.length})`}> + {ghosts + .filter(searchFor(searchText)) + .sort(compareNumberedText) + .map((thing) => ( + <OrbitedButton key={thing.name} color="grey" thing={thing} /> + ))} + </Section> + + <BasicSection title="Dead" source={dead} searchText={searchText} /> + + <BasicSection title="NPCs" source={npcs} searchText={searchText} /> + + <BasicSection title="Misc" source={misc} searchText={searchText} /> + </Window.Content> + </Window> + ); +};