mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-24 05:30:05 +01:00
Orbit refactor + features part dos: Icons, health bars. (#70580)
* orbit refactor + features * Via suggestion Co-Authored-By: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-Authored-By: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> * cleanup * readds heatmap optional + bug fix * fixes bugs * naming conventions * what * indeed * modular via suggestion Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> * nice * pai fix * misc searching Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com> Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
This commit is contained in:
co-authored by
John Willard
LemonInTheDark
parent
986d9516e7
commit
03f2f3e2b0
@@ -16,6 +16,8 @@
|
||||
var/intern_alt_name = null
|
||||
/// The icon_state associated with this trim, as it will show on the security HUD.
|
||||
var/sechud_icon_state = SECHUD_UNKNOWN
|
||||
/// Icons to be displayed in the orbit ui. Source: FontAwesome v5.
|
||||
var/orbit_icon
|
||||
|
||||
/// Accesses that this trim unlocks on a card it is imprinted on. These accesses never take wildcard slots and can be added and removed at will.
|
||||
var/list/access = list()
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
/datum/id_trim/job/assistant
|
||||
assignment = "Assistant"
|
||||
trim_state = "trim_assistant"
|
||||
orbit_icon = "toolbox"
|
||||
sechud_icon_state = SECHUD_ASSISTANT
|
||||
minimal_access = list()
|
||||
extra_access = list(
|
||||
@@ -105,6 +106,7 @@
|
||||
/datum/id_trim/job/atmospheric_technician
|
||||
assignment = "Atmospheric Technician"
|
||||
trim_state = "trim_atmospherictechnician"
|
||||
orbit_icon = "fan"
|
||||
department_color = COLOR_ENGINEERING_ORANGE
|
||||
subdepartment_color = COLOR_ENGINEERING_ORANGE
|
||||
sechud_icon_state = SECHUD_ATMOSPHERIC_TECHNICIAN
|
||||
@@ -134,6 +136,7 @@
|
||||
/datum/id_trim/job/bartender
|
||||
assignment = "Bartender"
|
||||
trim_state = "trim_bartender"
|
||||
orbit_icon = "cocktail"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_BARTENDER
|
||||
@@ -158,6 +161,7 @@
|
||||
/datum/id_trim/job/botanist
|
||||
assignment = "Botanist"
|
||||
trim_state = "trim_botanist"
|
||||
orbit_icon = "seedling"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_BOTANIST
|
||||
@@ -182,6 +186,7 @@
|
||||
assignment = "Captain"
|
||||
intern_alt_name = "Captain-in-Training"
|
||||
trim_state = "trim_captain"
|
||||
orbit_icon = "crown"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_COMMAND_BLUE
|
||||
department_state = "departmenthead"
|
||||
@@ -204,6 +209,7 @@
|
||||
/datum/id_trim/job/cargo_technician
|
||||
assignment = "Cargo Technician"
|
||||
trim_state = "trim_cargotechnician"
|
||||
orbit_icon = "box"
|
||||
department_color = COLOR_CARGO_BROWN
|
||||
subdepartment_color = COLOR_CARGO_BROWN
|
||||
sechud_icon_state = SECHUD_CARGO_TECHNICIAN
|
||||
@@ -228,6 +234,7 @@
|
||||
/datum/id_trim/job/chaplain
|
||||
assignment = "Chaplain"
|
||||
trim_state = "trim_chaplain"
|
||||
orbit_icon = "cross"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_CHAPLAIN
|
||||
@@ -249,6 +256,7 @@
|
||||
/datum/id_trim/job/chemist
|
||||
assignment = "Chemist"
|
||||
trim_state = "trim_chemist"
|
||||
orbit_icon = "prescription-bottle"
|
||||
department_color = COLOR_MEDICAL_BLUE
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
sechud_icon_state = SECHUD_CHEMIST
|
||||
@@ -275,6 +283,7 @@
|
||||
assignment = "Chief Engineer"
|
||||
intern_alt_name = "Chief Engineer-in-Training"
|
||||
trim_state = "trim_stationengineer"
|
||||
orbit_icon = "user-astronaut"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_ENGINEERING_ORANGE
|
||||
department_state = "departmenthead"
|
||||
@@ -316,6 +325,7 @@
|
||||
assignment = "Chief Medical Officer"
|
||||
intern_alt_name = "Chief Medical Officer-in-Training"
|
||||
trim_state = "trim_medicaldoctor"
|
||||
orbit_icon = "user-md"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
department_state = "departmenthead"
|
||||
@@ -353,6 +363,7 @@
|
||||
/datum/id_trim/job/clown
|
||||
assignment = "Clown"
|
||||
trim_state = "trim_clown"
|
||||
orbit_icon = "face-grin-tears"
|
||||
department_color = COLOR_MAGENTA
|
||||
subdepartment_color = COLOR_MAGENTA
|
||||
sechud_icon_state = SECHUD_CLOWN
|
||||
@@ -371,6 +382,7 @@
|
||||
/datum/id_trim/job/cook
|
||||
assignment = "Cook"
|
||||
trim_state = "trim_cook"
|
||||
orbit_icon = "utensils"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_COOK
|
||||
@@ -398,6 +410,7 @@
|
||||
/datum/id_trim/job/curator
|
||||
assignment = "Curator"
|
||||
trim_state = "trim_curator"
|
||||
orbit_icon = "book"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_CURATOR
|
||||
@@ -418,6 +431,7 @@
|
||||
/datum/id_trim/job/detective
|
||||
assignment = "Detective"
|
||||
trim_state = "trim_detective"
|
||||
orbit_icon = "user-secret"
|
||||
department_color = COLOR_SECURITY_RED
|
||||
subdepartment_color = COLOR_SECURITY_RED
|
||||
sechud_icon_state = SECHUD_DETECTIVE
|
||||
@@ -455,6 +469,7 @@
|
||||
/datum/id_trim/job/geneticist
|
||||
assignment = "Geneticist"
|
||||
trim_state = "trim_geneticist"
|
||||
orbit_icon = "dna"
|
||||
department_color = COLOR_SCIENCE_PINK
|
||||
subdepartment_color = COLOR_SCIENCE_PINK
|
||||
sechud_icon_state = SECHUD_GENETICIST
|
||||
@@ -482,6 +497,7 @@
|
||||
assignment = "Head of Personnel"
|
||||
intern_alt_name = "Head of Personnel-in-Training"
|
||||
trim_state = "trim_headofpersonnel"
|
||||
orbit_icon = "dog"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
department_state = "departmenthead"
|
||||
@@ -532,6 +548,7 @@
|
||||
assignment = "Head of Security"
|
||||
intern_alt_name = "Head of Security-in-Training"
|
||||
trim_state = "trim_securityofficer"
|
||||
orbit_icon = "user-shield"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_SECURITY_RED
|
||||
department_state = "departmenthead"
|
||||
@@ -588,6 +605,7 @@
|
||||
/datum/id_trim/job/janitor
|
||||
assignment = "Janitor"
|
||||
trim_state = "trim_janitor"
|
||||
orbit_icon = "broom"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SERVICE_LIME
|
||||
sechud_icon_state = SECHUD_JANITOR
|
||||
@@ -608,6 +626,7 @@
|
||||
/datum/id_trim/job/lawyer
|
||||
assignment = "Lawyer"
|
||||
trim_state = "trim_lawyer"
|
||||
orbit_icon = "gavel"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_SECURITY_RED
|
||||
sechud_icon_state = SECHUD_LAWYER
|
||||
@@ -628,6 +647,7 @@
|
||||
/datum/id_trim/job/medical_doctor
|
||||
assignment = "Medical Doctor"
|
||||
trim_state = "trim_medicaldoctor"
|
||||
orbit_icon = "staff-snake"
|
||||
department_color = COLOR_MEDICAL_BLUE
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
sechud_icon_state = SECHUD_MEDICAL_DOCTOR
|
||||
@@ -653,6 +673,7 @@
|
||||
/datum/id_trim/job/mime
|
||||
assignment = "Mime"
|
||||
trim_state = "trim_mime"
|
||||
orbit_icon = "comment-slash"
|
||||
department_color = COLOR_SILVER
|
||||
subdepartment_color = COLOR_WHITE
|
||||
sechud_icon_state = SECHUD_MIME
|
||||
@@ -671,6 +692,7 @@
|
||||
/datum/id_trim/job/paramedic
|
||||
assignment = "Paramedic"
|
||||
trim_state = "trim_paramedic"
|
||||
orbit_icon = "truck-medical"
|
||||
department_color = COLOR_MEDICAL_BLUE
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
sechud_icon_state = SECHUD_PARAMEDIC
|
||||
@@ -700,6 +722,7 @@
|
||||
/datum/id_trim/job/prisoner
|
||||
assignment = "Prisoner"
|
||||
trim_state = "trim_warden"
|
||||
orbit_icon = "lock-keyhole"
|
||||
department_color = COLOR_PRISONER_BLACK
|
||||
subdepartment_color = COLOR_PRISONER_ORANGE
|
||||
sechud_icon_state = SECHUD_PRISONER
|
||||
@@ -742,6 +765,7 @@
|
||||
/datum/id_trim/job/psychologist
|
||||
assignment = "Psychologist"
|
||||
trim_state = "trim_psychologist"
|
||||
orbit_icon = "brain"
|
||||
department_color = COLOR_SERVICE_LIME
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
sechud_icon_state = SECHUD_PSYCHOLOGIST
|
||||
@@ -762,6 +786,7 @@
|
||||
/datum/id_trim/job/quartermaster
|
||||
assignment = "Quartermaster"
|
||||
trim_state = "trim_quartermaster"
|
||||
orbit_icon = "sack-dollar"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_CARGO_BROWN
|
||||
department_state = "departmenthead"
|
||||
@@ -797,6 +822,7 @@
|
||||
assignment = "Research Director"
|
||||
intern_alt_name = "Research Director-in-Training"
|
||||
trim_state = "trim_scientist"
|
||||
orbit_icon = "user-graduate"
|
||||
department_color = COLOR_COMMAND_BLUE
|
||||
subdepartment_color = COLOR_SCIENCE_PINK
|
||||
department_state = "departmenthead"
|
||||
@@ -844,6 +870,7 @@
|
||||
/datum/id_trim/job/roboticist
|
||||
assignment = "Roboticist"
|
||||
trim_state = "trim_roboticist"
|
||||
orbit_icon = "battery-half"
|
||||
department_color = COLOR_SCIENCE_PINK
|
||||
subdepartment_color = COLOR_SCIENCE_PINK
|
||||
sechud_icon_state = SECHUD_ROBOTICIST
|
||||
@@ -871,6 +898,7 @@
|
||||
/datum/id_trim/job/scientist
|
||||
assignment = "Scientist"
|
||||
trim_state = "trim_scientist"
|
||||
orbit_icon = "flask"
|
||||
department_color = COLOR_SCIENCE_PINK
|
||||
subdepartment_color = COLOR_SCIENCE_PINK
|
||||
sechud_icon_state = SECHUD_SCIENTIST
|
||||
@@ -899,6 +927,7 @@
|
||||
/datum/id_trim/job/security_officer
|
||||
assignment = "Security Officer"
|
||||
trim_state = "trim_securityofficer"
|
||||
orbit_icon = "shield-halved"
|
||||
department_color = COLOR_SECURITY_RED
|
||||
subdepartment_color = COLOR_SECURITY_RED
|
||||
sechud_icon_state = SECHUD_SECURITY_OFFICER
|
||||
@@ -1019,6 +1048,7 @@
|
||||
/datum/id_trim/job/shaft_miner
|
||||
assignment = "Shaft Miner"
|
||||
trim_state = "trim_shaftminer"
|
||||
orbit_icon = "digging"
|
||||
department_color = COLOR_CARGO_BROWN
|
||||
subdepartment_color = COLOR_SCIENCE_PINK
|
||||
sechud_icon_state = SECHUD_SHAFT_MINER
|
||||
@@ -1055,6 +1085,7 @@
|
||||
/datum/id_trim/job/station_engineer
|
||||
assignment = "Station Engineer"
|
||||
trim_state = "trim_stationengineer"
|
||||
orbit_icon = "gears"
|
||||
department_color = COLOR_ENGINEERING_ORANGE
|
||||
subdepartment_color = COLOR_ENGINEERING_ORANGE
|
||||
sechud_icon_state = SECHUD_STATION_ENGINEER
|
||||
@@ -1084,6 +1115,7 @@
|
||||
/datum/id_trim/job/virologist
|
||||
assignment = "Virologist"
|
||||
trim_state = "trim_virologist"
|
||||
orbit_icon = "virus"
|
||||
department_color = COLOR_MEDICAL_BLUE
|
||||
subdepartment_color = COLOR_MEDICAL_BLUE
|
||||
sechud_icon_state = SECHUD_VIROLOGIST
|
||||
@@ -1108,6 +1140,7 @@
|
||||
/datum/id_trim/job/warden
|
||||
assignment = "Warden"
|
||||
trim_state = "trim_warden"
|
||||
orbit_icon = "handcuffs"
|
||||
department_color = COLOR_SECURITY_RED
|
||||
subdepartment_color = COLOR_SECURITY_RED
|
||||
sechud_icon_state = SECHUD_WARDEN
|
||||
|
||||
@@ -81,14 +81,18 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
|
||||
var/datum/mind/mind = mob_poi.mind
|
||||
var/was_antagonist = FALSE
|
||||
|
||||
serialized["job"] = mind?.assigned_role?.title
|
||||
serialized["name"] = mob_poi.real_name
|
||||
serialized["health"] = null
|
||||
// Cast the mob so we can get health
|
||||
var/mob/living/player
|
||||
if(isliving(mob_poi)) // Kind of silly here since we've already checked for dead mobs
|
||||
player = mob_poi
|
||||
|
||||
if(isliving(mob_poi)) // handles edge cases like blob
|
||||
var/mob/living/player = mob_poi
|
||||
serialized["health"] = FLOOR((player.health / player.maxHealth * 100), 1)
|
||||
if(issilicon(player))
|
||||
serialized["job"] = player.job
|
||||
else
|
||||
var/obj/item/card/id/id_card = player.get_idcard(hand_first = FALSE)
|
||||
serialized["job"] = id_card?.get_trim_assignment()
|
||||
var/datum/id_trim/trim = id_card?.trim
|
||||
serialized["job_icon"] = trim?.orbit_icon
|
||||
|
||||
for(var/datum/antagonist/antag_datum as anything in mind.antag_datums)
|
||||
if (antag_datum.show_to_ghosts)
|
||||
@@ -105,8 +109,7 @@ GLOBAL_DATUM_INIT(orbit_menu, /datum/orbit_menu, new)
|
||||
|
||||
misc += list(list(
|
||||
"ref" = REF(atom_poi),
|
||||
"name" = name,
|
||||
"extra" = null, // Just in case you want to add anything
|
||||
"full_name" = name,
|
||||
))
|
||||
|
||||
// Display the supermatter crystal integrity
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export const ANTAG2COLOR = {
|
||||
'Abductors': 'pink',
|
||||
'Ash Walkers': 'olive',
|
||||
'Biohazards': 'brown',
|
||||
'CentCom': 'teal',
|
||||
} as const;
|
||||
|
||||
export const ANTAG2GROUP = {
|
||||
'Abductor Agent': 'Abductors',
|
||||
'Abductor Scientist': 'Abductors',
|
||||
'Ash Walker': 'Ash Walkers',
|
||||
'Blob': 'Biohazards',
|
||||
'Sentient Disease': 'Biohazards',
|
||||
'CentCom Commander': 'CentCom',
|
||||
'CentCom Head Intern': 'CentCom',
|
||||
'CentCom Intern': 'CentCom',
|
||||
'CentCom Official': 'CentCom',
|
||||
'Central Command': 'CentCom',
|
||||
'Clown Operative': 'Clown Operatives',
|
||||
'Clown Operative Leader': 'Clown Operatives',
|
||||
'Nuclear Operative': 'Nuclear Operatives',
|
||||
'Nuclear Operative Leader': 'Nuclear Operatives',
|
||||
'Space Wizard': 'Wizard Federation',
|
||||
'Wizard Apprentice': 'Wizard Federation',
|
||||
'Wizard Minion': 'Wizard Federation',
|
||||
} as const;
|
||||
|
||||
export const JOB2ICON = {
|
||||
'AI': 'eye',
|
||||
'Cyborg': 'robot',
|
||||
'Personal AI': 'mobile-alt',
|
||||
} as const;
|
||||
|
||||
export enum THREAT {
|
||||
Low = 2,
|
||||
Medium = 5,
|
||||
High = 8,
|
||||
}
|
||||
|
||||
export enum HEALTH {
|
||||
Good = 69,
|
||||
Average = 19,
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { sortBy } from 'common/collections';
|
||||
import { ANTAG2GROUP, HEALTH, THREAT } from './constants';
|
||||
import type { AntagGroup, Antags, Observable } from './types';
|
||||
|
||||
/**
|
||||
* Collates antagonist groups into their own separate sections.
|
||||
* Some antags are grouped together lest they be listed separately,
|
||||
* ie: Nuclear Operatives. See: ANTAG_GROUPS.
|
||||
*/
|
||||
export const collateAntagonists = (antagonists: Antags) => {
|
||||
const collatedAntagonists = {}; // Hate that I cant use a map here
|
||||
antagonists.map((player) => {
|
||||
const { antag } = player;
|
||||
const resolvedName: string = ANTAG2GROUP[antag] || antag;
|
||||
if (!collatedAntagonists[resolvedName]) {
|
||||
collatedAntagonists[resolvedName] = [];
|
||||
}
|
||||
collatedAntagonists[resolvedName].push(player);
|
||||
});
|
||||
const sortedAntagonists = sortBy<AntagGroup>(([key]) => key)(
|
||||
Object.entries(collatedAntagonists)
|
||||
);
|
||||
|
||||
return sortedAntagonists;
|
||||
};
|
||||
|
||||
/** Returns a disguised name in case the person is wearing someone else's ID */
|
||||
export const getDisplayName = (full_name: string, name?: string) => {
|
||||
if (!name) {
|
||||
return full_name;
|
||||
}
|
||||
if (
|
||||
!full_name?.includes('[') ||
|
||||
full_name.match(/\(as /) ||
|
||||
full_name.match(/^Unknown/)
|
||||
) {
|
||||
return name;
|
||||
}
|
||||
// return only the name before the first ' [' or ' ('
|
||||
return `"${full_name.split(/ \[| \(/)[0]}"`;
|
||||
};
|
||||
|
||||
/** Returns the display color for certain health percentages */
|
||||
const getHealthColor = (health: number) => {
|
||||
switch (true) {
|
||||
case health > HEALTH.Good:
|
||||
return 'good';
|
||||
case health > HEALTH.Average:
|
||||
return 'average';
|
||||
default:
|
||||
return 'bad';
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns the display color based on orbiter numbers */
|
||||
const getThreatColor = (orbiters: number) => {
|
||||
switch (true) {
|
||||
case orbiters > THREAT.High:
|
||||
return 'violet';
|
||||
case orbiters > THREAT.Medium:
|
||||
return 'blue';
|
||||
case orbiters > THREAT.Low:
|
||||
return 'teal';
|
||||
default:
|
||||
return 'good';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ### getDisplayColor
|
||||
* Displays color for buttons based on the health or orbiter count. Toggleable.
|
||||
* @param {Observable} item - The point of interest.
|
||||
* @param {boolean} heatMap - Whether the user has heat map toggled.
|
||||
* @param {string} color - OPTIONAL: The color to default to.
|
||||
*/
|
||||
export const getDisplayColor = (
|
||||
item: Observable,
|
||||
heatMap: boolean,
|
||||
color?: string
|
||||
) => {
|
||||
const { health, orbiters = 0 } = item;
|
||||
if (typeof health !== 'number') {
|
||||
return color ? 'good' : 'grey';
|
||||
}
|
||||
if (heatMap) {
|
||||
return getThreatColor(orbiters);
|
||||
}
|
||||
return getHealthColor(health);
|
||||
};
|
||||
|
||||
/** Checks if a full name or job title matches the search. */
|
||||
export const isJobOrNameMatch = (
|
||||
observable: Observable,
|
||||
searchQuery: string
|
||||
) => {
|
||||
if (!searchQuery) {
|
||||
return true;
|
||||
}
|
||||
const { full_name, job } = observable;
|
||||
|
||||
return (
|
||||
full_name?.toLowerCase().includes(searchQuery?.toLowerCase()) ||
|
||||
job?.toLowerCase().includes(searchQuery?.toLowerCase()) ||
|
||||
false
|
||||
);
|
||||
};
|
||||
+60
-177
@@ -1,73 +1,19 @@
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { capitalizeFirst, multiline } from 'common/string';
|
||||
import { Box, Button, Collapsible, Icon, Input, LabeledList, NoticeBox, Section, Stack } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { flow } from 'common/fp';
|
||||
|
||||
type AntagGroup = [string, Antags];
|
||||
|
||||
type Antags = Array<Observable & { antag: string }>;
|
||||
|
||||
type Data = {
|
||||
alive: Array<Observable>;
|
||||
antagonists: Antags;
|
||||
dead: Array<Observable>;
|
||||
ghosts: Array<Observable>;
|
||||
misc: Array<Observable>;
|
||||
npcs: Array<Observable>;
|
||||
};
|
||||
|
||||
type Observable = {
|
||||
extra?: string;
|
||||
full_name: string;
|
||||
health?: number;
|
||||
job?: string;
|
||||
name?: string;
|
||||
orbiters?: number;
|
||||
ref: string;
|
||||
};
|
||||
|
||||
const ANTAG2COLOR = {
|
||||
'Abductors': 'pink',
|
||||
'Ash Walkers': 'olive',
|
||||
'Biohazards': 'brown',
|
||||
'CentCom': 'teal',
|
||||
} as const;
|
||||
|
||||
const ANTAG2GROUP = {
|
||||
'Abductor Agent': 'Abductors',
|
||||
'Abductor Scientist': 'Abductors',
|
||||
'Ash Walker': 'Ash Walkers',
|
||||
'Blob': 'Biohazards',
|
||||
'Sentient Disease': 'Biohazards',
|
||||
'CentCom Commander': 'CentCom',
|
||||
'CentCom Head Intern': 'CentCom',
|
||||
'CentCom Intern': 'CentCom',
|
||||
'CentCom Official': 'CentCom',
|
||||
'Central Command': 'CentCom',
|
||||
'Clown Operative': 'Clown Operatives',
|
||||
'Clown Operative Leader': 'Clown Operatives',
|
||||
'Nuclear Operative': 'Nuclear Operatives',
|
||||
'Nuclear Operative Leader': 'Nuclear Operatives',
|
||||
'Space Wizard': 'Wizard Federation',
|
||||
'Wizard Apprentice': 'Wizard Federation',
|
||||
'Wizard Minion': 'Wizard Federation',
|
||||
} as const;
|
||||
|
||||
enum THREAT {
|
||||
None,
|
||||
Small = 'teal',
|
||||
Medium = 'blue',
|
||||
Large = 'violet',
|
||||
}
|
||||
import { capitalizeFirst, multiline } from 'common/string';
|
||||
import { useBackend, useLocalState } from 'tgui/backend';
|
||||
import { Button, Collapsible, Icon, Input, LabeledList, NoticeBox, Section, Stack } from 'tgui/components';
|
||||
import { Window } from 'tgui/layouts';
|
||||
import { collateAntagonists, getDisplayColor, getDisplayName, isJobOrNameMatch } from './helpers';
|
||||
import { ANTAG2COLOR, JOB2ICON } from './constants';
|
||||
import type { AntagGroup, Observable, OrbitData } from './types';
|
||||
|
||||
export const Orbit = (props, context) => {
|
||||
return (
|
||||
<Window title="Orbit" width={400} height={550}>
|
||||
<Window.Content scrollable>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item mt={0}>
|
||||
<Stack.Item>
|
||||
<ObservableSearch />
|
||||
</Stack.Item>
|
||||
<Stack.Item mt={0.2} grow>
|
||||
@@ -83,7 +29,7 @@ export const Orbit = (props, context) => {
|
||||
|
||||
/** Controls filtering out the list of observables via search */
|
||||
const ObservableSearch = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
const { act, data } = useBackend<OrbitData>(context);
|
||||
const {
|
||||
alive = [],
|
||||
antagonists = [],
|
||||
@@ -97,13 +43,18 @@ const ObservableSearch = (props, context) => {
|
||||
'autoObserve',
|
||||
false
|
||||
);
|
||||
const [heatMap, setHeatMap] = useLocalState<boolean>(
|
||||
context,
|
||||
'heatMap',
|
||||
false
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useLocalState<string>(
|
||||
context,
|
||||
'searchQuery',
|
||||
''
|
||||
);
|
||||
/** Gets a list of Observables, then filters the most relevant to orbit */
|
||||
const orbitMostRelevant = (searchQuery: string): void => {
|
||||
const orbitMostRelevant = (searchQuery: string) => {
|
||||
/** Returns the most orbited observable that matches the search. */
|
||||
const mostRelevant: Observable = flow([
|
||||
// Filters out anything that doesn't match search
|
||||
@@ -139,6 +90,16 @@ const ObservableSearch = (props, context) => {
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Divider />
|
||||
<Stack.Item>
|
||||
<Button
|
||||
color="transparent"
|
||||
icon={!heatMap ? 'heart' : 'ghost'}
|
||||
onClick={() => setHeatMap(!heatMap)}
|
||||
tooltip={multiline`Toggles between highlighting health or
|
||||
orbiters.`}
|
||||
tooltipPosition="bottom-start"
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
color={autoObserve ? 'good' : 'transparent'}
|
||||
@@ -151,12 +112,11 @@ const ObservableSearch = (props, context) => {
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button
|
||||
inline
|
||||
color="transparent"
|
||||
tooltip="Refresh"
|
||||
tooltipPosition="bottom-start"
|
||||
icon="sync-alt"
|
||||
onClick={() => act('refresh')}
|
||||
tooltip="Refresh"
|
||||
tooltipPosition="bottom-start"
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
@@ -170,7 +130,7 @@ const ObservableSearch = (props, context) => {
|
||||
* observable group.
|
||||
*/
|
||||
const ObservableContent = (props, context) => {
|
||||
const { data } = useBackend<Data>(context);
|
||||
const { data } = useBackend<OrbitData>(context);
|
||||
const {
|
||||
alive = [],
|
||||
antagonists = [],
|
||||
@@ -196,7 +156,7 @@ const ObservableContent = (props, context) => {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ObservableSection color="good" section={alive} title="Alive" />
|
||||
<ObservableSection color="blue" section={alive} title="Alive" />
|
||||
<ObservableSection section={dead} title="Dead" />
|
||||
<ObservableSection section={ghosts} title="Ghosts" />
|
||||
<ObservableSection section={misc} title="Misc" />
|
||||
@@ -217,7 +177,7 @@ const ObservableSection = (
|
||||
},
|
||||
context
|
||||
) => {
|
||||
const { color = 'grey', section = [], title } = props;
|
||||
const { color, section = [], title } = props;
|
||||
if (!section.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -226,7 +186,11 @@ const ObservableSection = (
|
||||
filter<Observable>((observable) =>
|
||||
isJobOrNameMatch(observable, searchQuery)
|
||||
),
|
||||
sortBy<Observable>((observable) => observable.name?.toLowerCase()),
|
||||
sortBy<Observable>((observable) =>
|
||||
getDisplayName(observable.full_name, observable.name)
|
||||
.replace(/^"/, '')
|
||||
.toLowerCase()
|
||||
),
|
||||
])(section);
|
||||
if (!filteredSection.length) {
|
||||
return null;
|
||||
@@ -236,8 +200,8 @@ const ObservableSection = (
|
||||
<Stack.Item>
|
||||
<Collapsible
|
||||
bold
|
||||
color={color}
|
||||
open={color !== 'grey'}
|
||||
color={color ?? 'grey'}
|
||||
open={!!color}
|
||||
title={title + ` - (${filteredSection.length})`}>
|
||||
{filteredSection.map((poi, index) => {
|
||||
return <ObservableItem color={color} item={poi} key={index} />;
|
||||
@@ -249,28 +213,28 @@ const ObservableSection = (
|
||||
|
||||
/** Renders an observable button that has tooltip info for living Observables*/
|
||||
const ObservableItem = (
|
||||
props: { color: string; item: Observable },
|
||||
props: { color?: string; item: Observable },
|
||||
context
|
||||
) => {
|
||||
const { act } = useBackend<Data>(context);
|
||||
const { act } = useBackend<OrbitData>(context);
|
||||
const { color, item } = props;
|
||||
const { extra, full_name, health, name, orbiters, ref } = item;
|
||||
const { extra, full_name, job, job_icon, health, name, orbiters, ref } = item;
|
||||
const [autoObserve] = useLocalState<boolean>(context, 'autoObserve', false);
|
||||
const threat = getThreat(orbiters ?? 0);
|
||||
const displayName = getDisplayName(name, full_name);
|
||||
const [heatMap] = useLocalState<boolean>(context, 'heatMap', false);
|
||||
|
||||
return (
|
||||
<Button
|
||||
color={threat || color}
|
||||
color={getDisplayColor(item, heatMap, color)}
|
||||
icon={job_icon || (job && JOB2ICON[job]) || null}
|
||||
onClick={() => act('orbit', { auto_observe: autoObserve, ref: ref })}
|
||||
tooltip={(!!health || !!extra) && <ObservableTooltip item={item} />}
|
||||
tooltipPosition="bottom-start">
|
||||
{capitalizeFirst(displayName)}
|
||||
{capitalizeFirst(getDisplayName(full_name, name))}
|
||||
{!!orbiters && (
|
||||
<>
|
||||
{' '}
|
||||
({orbiters}{' '}
|
||||
<Icon mr={0} name={threat === THREAT.Large ? 'skull' : 'ghost'} />)
|
||||
<Icon mr={0} name={'ghost'} />
|
||||
{orbiters}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -283,6 +247,7 @@ const ObservableTooltip = (props: { item: Observable }) => {
|
||||
item: { extra, full_name, job, health },
|
||||
} = props;
|
||||
const extraInfo = extra?.split(':');
|
||||
const displayHealth = !!health && health >= 0 ? `${health}%` : 'Critical';
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -290,106 +255,24 @@ const ObservableTooltip = (props: { item: Observable }) => {
|
||||
Last Known Data
|
||||
</NoticeBox>
|
||||
<LabeledList>
|
||||
{!!extraInfo && (
|
||||
{extraInfo ? (
|
||||
<LabeledList.Item label={extraInfo[0]}>
|
||||
{extraInfo[1]}
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
{!!full_name && (
|
||||
<LabeledList.Item label="Name">{full_name}</LabeledList.Item>
|
||||
)}
|
||||
{!!job && <LabeledList.Item label="Job">{job}</LabeledList.Item>}
|
||||
{!!health && (
|
||||
<LabeledList.Item label="Health">
|
||||
{getHealthLabel(health!)}
|
||||
</LabeledList.Item>
|
||||
) : (
|
||||
<>
|
||||
{!!full_name && (
|
||||
<LabeledList.Item label="Name">{full_name}</LabeledList.Item>
|
||||
)}
|
||||
{!!job && <LabeledList.Item label="Job">{job}</LabeledList.Item>}
|
||||
{!!health && (
|
||||
<LabeledList.Item label="Health">
|
||||
{displayHealth}
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</LabeledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Collates antagonist groups into their own separate sections.
|
||||
* Some antags are grouped together lest they be listed separately,
|
||||
* ie: Nuclear Operatives. See: ANTAG_GROUPS.
|
||||
*/
|
||||
const collateAntagonists = (antagonists: Antags) => {
|
||||
const collatedAntagonists = {}; // Hate that I cant use a map here
|
||||
antagonists.map((player) => {
|
||||
const { antag } = player;
|
||||
const resolvedName: string = ANTAG2GROUP[antag] || antag;
|
||||
if (!collatedAntagonists[resolvedName]) {
|
||||
collatedAntagonists[resolvedName] = [];
|
||||
}
|
||||
collatedAntagonists[resolvedName].push(player);
|
||||
});
|
||||
const sortedAntagonists = sortBy<AntagGroup>(([key]) => key)(
|
||||
Object.entries(collatedAntagonists)
|
||||
);
|
||||
|
||||
return sortedAntagonists;
|
||||
};
|
||||
|
||||
/** Returns a disguised name in case the person is wearing someone else's ID */
|
||||
const getDisplayName = (name: string | undefined, full_name: string) => {
|
||||
if (!name) {
|
||||
return full_name;
|
||||
}
|
||||
if (
|
||||
!full_name?.includes('[') ||
|
||||
full_name.match(/\(as /) ||
|
||||
full_name.match(/^Unknown/)
|
||||
) {
|
||||
return name;
|
||||
}
|
||||
// return only the name before the first ' [' or ' ('
|
||||
return `"${full_name.split(/ \[| \(/)[0]}"`;
|
||||
};
|
||||
|
||||
/** Returns some labels for a player's health */
|
||||
const getHealthLabel = (health: number) => {
|
||||
if (health >= 100) {
|
||||
return <Box color="blue">Great</Box>;
|
||||
}
|
||||
if (health >= 75) {
|
||||
return <Box color="green">Good</Box>;
|
||||
}
|
||||
if (health >= 50) {
|
||||
return <Box color="yellow">Fair</Box>;
|
||||
}
|
||||
if (health >= 25) {
|
||||
return <Box color="orange">Poor</Box>;
|
||||
}
|
||||
if (health > 0) {
|
||||
return <Box color="orange">Bad</Box>;
|
||||
}
|
||||
if (health <= 0) {
|
||||
return <Box color="red">Critical</Box>;
|
||||
}
|
||||
};
|
||||
|
||||
/** Takes the amount of orbiters and returns some style options */
|
||||
const getThreat = (orbiters: number) => {
|
||||
if (!orbiters || orbiters <= 2) {
|
||||
return THREAT.None;
|
||||
} else if (orbiters === 3) {
|
||||
return THREAT.Small;
|
||||
} else if (orbiters <= 6) {
|
||||
return THREAT.Medium;
|
||||
} else {
|
||||
return THREAT.Large;
|
||||
}
|
||||
};
|
||||
|
||||
/** Checks if a full name or job title matches the search. */
|
||||
const isJobOrNameMatch = (observable: Observable, searchQuery: string) => {
|
||||
const { full_name, name, job } = observable;
|
||||
const displayName = full_name ?? name;
|
||||
|
||||
return (
|
||||
displayName?.toLowerCase().includes(searchQuery?.toLowerCase()) ||
|
||||
job?.toLowerCase().includes(searchQuery?.toLowerCase()) ||
|
||||
false
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
export type AntagGroup = [string, Antags];
|
||||
|
||||
export type Antags = Array<Observable & { antag: string }>;
|
||||
|
||||
export type OrbitData = {
|
||||
alive: Array<Observable>;
|
||||
antagonists: Antags;
|
||||
dead: Array<Observable>;
|
||||
ghosts: Array<Observable>;
|
||||
misc: Array<Observable>;
|
||||
npcs: Array<Observable>;
|
||||
};
|
||||
|
||||
export type Observable = {
|
||||
extra?: string;
|
||||
full_name: string;
|
||||
health?: number;
|
||||
job?: string;
|
||||
job_icon?: string;
|
||||
name?: string;
|
||||
orbiters?: number;
|
||||
ref: string;
|
||||
};
|
||||
Reference in New Issue
Block a user