diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 2bb05b7c0ffa..bcdf5b154953 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -287,7 +287,7 @@ Turf and target are separate in case you want to teleport some distance from a t return . //Returns a list of all items of interest with their name -/proc/getpois(mobs_only=0,skip_mindless=0) +/proc/getpois(mobs_only = FALSE, skip_mindless = FALSE, specify_dead_role = TRUE) var/list/mobs = sortmobs() var/list/namecounts = list() var/list/pois = list() @@ -301,7 +301,7 @@ Turf and target are separate in case you want to teleport some distance from a t if(M.real_name && M.real_name != M.name) name += " \[[M.real_name]\]" - if(M.stat == DEAD) + if(M.stat == DEAD && specify_dead_role) if(isobserver(M)) name += " \[ghost\]" else diff --git a/code/datums/components/orbiter.dm b/code/datums/components/orbiter.dm index 0c9bb2b6406e..9ba5f5fa29ad 100644 --- a/code/datums/components/orbiter.dm +++ b/code/datums/components/orbiter.dm @@ -160,10 +160,11 @@ /atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE) if(!istype(A) || !get_turf(A) || A == src) return - + orbit_target = A return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation) /atom/movable/proc/stop_orbit(datum/component/orbiter/orbits) + orbit_target = null return // We're just a simple hook /atom/proc/transfer_observers_to(atom/target) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 015d5aaeda53..a49b60749eda 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -75,7 +75,7 @@ var/chat_color_darkened // A luminescence-shifted value of the last color calculated for chatmessage overlays - + var/atom/orbit_target //Reference to atom being orbited /** * Called when an atom is created in byond (built in engine proc) * @@ -1102,3 +1102,22 @@ */ /atom/proc/rust_heretic_act() return + +/** + * Recursive getter method to return a list of all ghosts orbitting this atom + * + * This will work fine without manually passing arguments. + */ +/atom/proc/get_all_orbiters(list/processed, source = TRUE) + var/list/output = list() + if (!processed) + processed = list() + if (src in processed) + return output + if (!source) + output += src + processed += src + for (var/o in orbiters?.orbiters) + var/atom/atom_orbiter = o + output += atom_orbiter.get_all_orbiters(processed, source = FALSE) + return output diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 9d5b90030df2..ba80cb7cc3ca 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -479,7 +479,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp var/list/dest = list() //List of possible destinations (mobs) var/target = null //Chosen target. - dest += getpois(mobs_only=1) //Fill list, prompt user with list + dest += getpois(mobs_only = TRUE) //Fill list, prompt user with list target = input("Please, select a player!", "Jump to Mob", null, null) as null|anything in dest if (!target)//Make sure we actually have a target @@ -820,7 +820,9 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp if (!eye_name) return - var/mob/mob_eye = creatures[eye_name] + do_observe(creatures[eye_name]) + +/mob/dead/observer/proc/do_observe(mob/mob_eye) //Istype so we filter out points of interest that are not mobs if(client && mob_eye && istype(mob_eye)) client.eye = mob_eye diff --git a/code/modules/mob/dead/observer/orbit.dm b/code/modules/mob/dead/observer/orbit.dm index eb23a5fb6772..003e0a2258f2 100644 --- a/code/modules/mob/dead/observer/orbit.dm +++ b/code/modules/mob/dead/observer/orbit.dm @@ -1,5 +1,6 @@ /datum/orbit_menu var/mob/dead/observer/owner + var/auto_observe = FALSE /datum/orbit_menu/New(mob/dead/observer/new_owner) if(!istype(new_owner)) @@ -7,23 +8,44 @@ 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) + ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open) 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 (..()) + . = ..() + if(.) return - - if (action == "orbit") - var/list/pois = getpois(skip_mindless = 1) - var/atom/movable/poi = pois[params["name"]] - if (poi != null) + switch(action) + if ("orbit") + var/ref = params["ref"] + var/atom/movable/poi = (locate(ref) in GLOB.mob_list) || (locate(ref) in GLOB.poi_list) + if (poi == null) + . = TRUE + return owner.ManualFollow(poi) - ui.close() + owner.reset_perspective(null) + if (auto_observe) + owner.do_observe(poi) + . = TRUE + if ("refresh") + update_static_data(owner, ui) + . = TRUE + if ("toggle_observe") + auto_observe = !auto_observe + if (auto_observe && owner.orbit_target) + owner.do_observe(owner.orbit_target) + else + owner.reset_perspective(null) /datum/orbit_menu/ui_data(mob/user) var/list/data = list() + data["auto_observe"] = auto_observe + return data + +/datum/orbit_menu/ui_static_data(mob/user) + var/list/data = list() var/list/alive = list() var/list/antagonists = list() @@ -32,23 +54,28 @@ var/list/misc = list() var/list/npcs = list() - var/list/pois = getpois(skip_mindless = 1) + var/list/pois = getpois(skip_mindless = TRUE, specify_dead_role = FALSE) for (var/name in pois) var/list/serialized = list() serialized["name"] = name var/poi = pois[name] + serialized["ref"] = REF(poi) + var/mob/M = poi if (istype(M)) if (isobserver(M)) + var/number_of_orbiters = length(M.get_all_orbiters()) + if (number_of_orbiters) + serialized["orbiters"] = number_of_orbiters 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 + var/number_of_orbiters = length(M.get_all_orbiters()) if (number_of_orbiters) serialized["orbiters"] = number_of_orbiters @@ -74,5 +101,4 @@ data["ghosts"] = ghosts data["misc"] = misc data["npcs"] = npcs - return data diff --git a/tgui/packages/tgui/assets.js b/tgui/packages/tgui/assets.js new file mode 100644 index 000000000000..13f17b20e4be --- /dev/null +++ b/tgui/packages/tgui/assets.js @@ -0,0 +1,95 @@ +/** + * @file + * @copyright 2020 Aleksej Komarov + * @license MIT + */ + +import { loadCSS as fgLoadCss } from 'fg-loadcss'; +import { createLogger } from './logging'; + +const logger = createLogger('assets'); + +const EXCLUDED_PATTERNS = [/v4shim/i]; +const RETRY_ATTEMPTS = 5; +const RETRY_INTERVAL = 3000; + +const loadedStyleSheetByUrl = {}; +const loadedMappings = {}; + +export const loadStyleSheet = (url, attempt = 1) => { + if (loadedStyleSheetByUrl[url]) { + return; + } + loadedStyleSheetByUrl[url] = true; + logger.log(`loading stylesheet '${url}'`); + /** @type {HTMLLinkElement} */ + let node = fgLoadCss(url); + node.addEventListener('load', () => { + if (!isStyleSheetReallyLoaded(node, url)) { + node.parentNode.removeChild(node); + node = null; + loadedStyleSheetByUrl[url] = null; + if (attempt >= RETRY_ATTEMPTS) { + logger.error(`Error: Failed to load the stylesheet ` + + `'${url}' after ${RETRY_ATTEMPTS} attempts.\nIt was either ` + + `not found, or you're trying to load an empty stylesheet ` + + `that has no CSS rules in it.`); + return; + } + setTimeout(() => loadStyleSheet(url, attempt + 1), RETRY_INTERVAL); + return; + } + }); +}; + +/** + * Checks whether the stylesheet was registered in the DOM + * and is not empty. + */ +const isStyleSheetReallyLoaded = (node, url) => { + // Method #1 (works on IE10+) + const styleSheet = node.sheet; + if (styleSheet) { + return styleSheet.rules.length > 0; + } + // Method #2 + const styleSheets = document.styleSheets; + const len = styleSheets.length; + for (let i = 0; i < len; i++) { + const styleSheet = styleSheets[i]; + if (styleSheet.href.includes(url)) { + return styleSheet.rules.length > 0; + } + } + // All methods failed + logger.warn(`Warning: stylesheet '${url}' was not found in the DOM`); + return false; +}; + +export const resolveAsset = name => ( + loadedMappings[name] || name +); + +export const assetMiddleware = store => next => action => { + const { type, payload } = action; + if (type === 'asset/stylesheet') { + loadStyleSheet(payload); + return; + } + if (type === 'asset/mappings') { + for (let name of Object.keys(payload)) { + // Skip anything that matches excluded patterns + if (EXCLUDED_PATTERNS.some(regex => regex.test(name))) { + continue; + } + const url = payload[name]; + const ext = name.split('.').pop(); + loadedMappings[name] = url; + if (ext === 'css') { + loadStyleSheet(url); + } + } + return; + } + next(action); +}; \ No newline at end of file diff --git a/tgui/packages/tgui/interfaces/Orbit.js b/tgui/packages/tgui/interfaces/Orbit.js index 131c63651df3..d71348c41200 100644 --- a/tgui/packages/tgui/interfaces/Orbit.js +++ b/tgui/packages/tgui/interfaces/Orbit.js @@ -1,9 +1,10 @@ import { createSearch } from 'common/string'; -import { Box, Button, Input, Section } from '../components'; -import { Window } from '../layouts'; +import { multiline } from 'common/string'; +import { resolveAsset } from '../assets'; import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Divider, Flex, Icon, Input, Section } from '../components'; +import { Window } from '../layouts'; -const PATTERN_DESCRIPTOR = / \[(?:ghost|dead)\]$/; const PATTERN_NUMBER = / \(([0-9]+)\)$/; const searchFor = searchText => createSearch(searchText, thing => thing.name); @@ -42,9 +43,9 @@ const BasicSection = (props, context) => { {things.map(thing => (