[MIRROR] moves robot sleepers to tgui (#11849)

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-10-21 15:37:17 -07:00
committed by GitHub
parent 999a0816fa
commit a5adea312e
19 changed files with 661 additions and 198 deletions

View File

@@ -102,7 +102,7 @@
*/
/datum/proc/Destroy(force=FALSE)
SHOULD_CALL_PARENT(TRUE)
//SHOULD_NOT_SLEEP(TRUE)
SHOULD_NOT_SLEEP(TRUE)
tag = null
weak_reference = null //ensure prompt GCing of weakref.

View File

@@ -238,7 +238,11 @@ GLOBAL_LIST_EMPTY(Holiday) //Holidays are lists now, so we can have more than on
GLOB.Holiday = list()
var/H = tgui_input_text(src,"What holiday is it today?","Set Holiday")
if(!H)
return
var/B = tgui_input_text(src,"Now explain what the holiday is about","Set Holiday", multiline = TRUE, prevent_enter = TRUE)
if(!B)
return
GLOB.Holiday[H] = B

View File

@@ -57,6 +57,9 @@
var/datum/asset/spritesheet_batched/robot_icons/spritesheet = GLOB.robot_sprite_sheets[target.modtype]
if(target)
var/ui_theme = target.get_ui_theme()
if(ui_theme)
.["theme"] = ui_theme
.["target"] = list()
.["target"]["name"] = target.name
.["target"]["ckey"] = target.ckey

View File

@@ -16,7 +16,6 @@
var/list/injection_chems = list(REAGENT_ID_INAPROVALINE, REAGENT_ID_BICARIDINE, REAGENT_ID_KELOTANE, REAGENT_ID_ANTITOXIN, REAGENT_ID_DEXALIN, REAGENT_ID_TRICORDRAZINE, REAGENT_ID_SPACEACILLIN, REAGENT_ID_TRAMADOL) //The borg is able to heal every damage type. As a nerf, they use 750 charge per injection.
var/eject_port = "ingestion"
var/list/items_preserved = list()
var/UI_open = FALSE
var/stabilizer = TRUE
var/compactor = FALSE
var/analyzer = FALSE
@@ -199,13 +198,8 @@
dlist.Cut()
if(length(contents) > 0)
hound.visible_message(span_warning("[hound.name] empties out their contents via their [eject_port] port."), span_notice("You empty your contents via your [eject_port] port."))
for(var/C in contents)
if(ishuman(C))
var/mob/living/carbon/human/person = C
person.forceMove(get_turf(src))
else
var/obj/T = C
T.loc = hound.loc
for(var/atom/movable/content in contents)
content.forceMove(get_turf(src))
playsound(src, 'sound/effects/splat.ogg', 50, 1)
update_patient()
@@ -231,206 +225,143 @@
/obj/item/dogborg/sleeper/attack_self(mob/user)
if(..())
return
sleeperUI(user)
tgui_interact(user)
/obj/item/dogborg/sleeper/proc/sleeperUI(mob/user)
var/dat = "<TITLE>[name] Console</TITLE><BR>"
/obj/item/dogborg/sleeper/tgui_state(mob/user)
return GLOB.tgui_conscious_state
if(islist(injection_chems)) //Only display this if we're a drug-dispensing doggo.
dat += "<h3>Injector</h3>"
if(patient)// && patient.health > min_health) //Not necessary, leave the buttons on, but the feedback during injection will give more information.
for(var/re in injection_chems)
var/datum/reagent/C = SSchemistry.chemical_reagents[re]
if(C)
dat += "<A href='byond://?src=\ref[src];inject=[C.id]'>Inject [C.name]</A><BR>"
else
for(var/re in injection_chems)
var/datum/reagent/C = SSchemistry.chemical_reagents[re]
if(C)
dat += span_linkOff("Inject [C.name]") + "<BR>"
/obj/item/dogborg/sleeper/tgui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "RobotSleeper", "[name] Console")
ui.open()
dat += "<h3>[name] Status</h3>"
dat += "<div style='display: flex; flex-wrap: wrap; flex-direction: row;'>"
dat += "<A id='refbutton' href='byond://?src=\ref[src];refresh=1'>Refresh</A>"
dat += "<A href='byond://?src=\ref[src];eject=1'>Eject All</A>"
dat += "<A href='byond://?src=\ref[src];port=1'>Eject port: [eject_port]</A>"
dat += "<A href='byond://?src=\ref[src];ingest=1'>Vore All</A>" //might as well make it obvious
if(!cleaning)
dat += "<A href='byond://?src=\ref[src];clean=1'>Self-Clean</A>"
else
dat += span_linkOff("Self-Clean")
if(medsensor)
dat += "<A href='byond://?src=\ref[src];analyze=1'>Analyze Patient</A>"
if(delivery)
dat += "<BR><h3>Cargo Compartment</h3><BR>"
dat += "<A href='byond://?src=\ref[src];deliveryslot=1'>Active Slot: [delivery_tag]</A>"
if(islist(deliverylists[delivery_tag]))
dat += "<A href='byond://?src=\ref[src];slot_eject=1'>Eject Slot</A>"
dat += "</div>"
dat += "<div class='statusDisplay'>"
/obj/item/dogborg/sleeper/tgui_static_data(mob/user)
var/list/data = ..()
if(!delivery && compactor && length(contents))//garbage counter for trashpup
dat += span_red(span_bold("Current load:") + " [length(contents)] / [max_item_count] objects.") + "<BR>"
dat += span_gray("([contents.Join(", ")])") + "<BR><BR>"
if(!isrobot(user))
return data
if(ore_storage)
dat += "<font color='red'><B>Current ore capacity:</B> [current_capacity] / [max_ore_storage].</font><BR>"
var/mob/living/silicon/robot/robot_user = user
var/list/robot_chems = list()
for(var/re in injection_chems)
var/datum/reagent/possible_reagent = SSchemistry.chemical_reagents[re]
UNTYPED_LIST_ADD(robot_chems, list("id" = possible_reagent.id, "name" = possible_reagent.name))
if(delivery && length(contents))
dat += span_red(span_bold("Current load:") + " [length(contents)] / [max_item_count] objects.") + "<BR>"
dat += span_gray("Cargo compartment slot: Cargo 1.") + "<BR>"
if(length(deliveryslot_1))
dat += span_gray("([deliveryslot_1.Join(", ")])") + "<BR>"
dat += span_gray("Cargo compartment slot: Cargo 2.") + "<BR>"
if(length(deliveryslot_2))
dat += span_gray("([deliveryslot_2.Join(", ")])") + "<BR>"
dat += span_gray("Cargo compartment slot: Cargo 3.") + "<BR>"
if(length(deliveryslot_3))
dat += span_gray("([deliveryslot_3.Join(", ")])") + "<BR>"
dat += span_red("Cargo compartment slot: Fuel.") + "<BR>"
dat += span_red("([jointext(contents - (deliveryslot_1 + deliveryslot_2 + deliveryslot_3),", ")])") + "<BR><BR>"
data["name"] = name
var/robot_theme = robot_user.get_ui_theme()
if(robot_theme)
data["theme"] = robot_theme
data["chems"] = robot_chems
return data
//Cleaning and there are still un-preserved items
if(cleaning && length(contents - items_preserved))
dat += span_red(span_bold("Self-cleaning mode.") + " [length(contents - items_preserved)] object(s) remaining.") + "<BR>"
/obj/item/dogborg/sleeper/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
var/list/patient_data
//There are no items to be processed other than un-preserved items
else if(cleaning && length(items_preserved))
dat += span_red(span_bold("Self-cleaning done. Eject remaining objects now.")) + "<BR>"
//Preserved items count when the list is populated
if(length(items_preserved))
dat += span_red("[length(items_preserved)] uncleanable object(s).") + "<BR>"
if(!patient)
dat += "[src.name] Unoccupied"
else
dat += "[patient.name] => "
switch(patient.stat)
if(0)
dat += span_green("Conscious")
if(1)
dat += span_orange("Unconscious")
else
dat += span_red("DEAD")
var/pulse = "\t-Pulse, bpm: [patient.get_pulse(GETPULSE_TOOL)]"
dat += (patient.pulse == PULSE_NONE || patient.pulse == PULSE_THREADY ? span_red(pulse) : span_white(pulse))
dat += "<BR>"
var/health = "\t-Overall Health %: [round(100 * (patient.health / patient.getMaxHealth()))]"
dat += (patient.health > 0 ? span_white(health) : span_red(health))
dat += "<BR>"
var/brute = "\t-Brute Damage %: [patient.getBruteLoss()]"
dat += (patient.getBruteLoss() < 60 ? span_gray(brute) : span_red(brute))
dat += "<BR>"
var/oxygen = "\t-Respiratory Damage %: [patient.getOxyLoss()]"
dat += (patient.getOxyLoss() < 60 ? span_gray(oxygen) : span_red(oxygen))
dat += "<BR>"
var/toxic = "\t-Toxin Content %: [patient.getToxLoss()]"
dat += (patient.getToxLoss() < 60 ? span_gray(toxic) : span_red(toxic))
dat += "<BR>"
var/burn = "\t-Burn Severity %: [patient.getFireLoss()]"
dat += (patient.getFireLoss() < 60 ? span_gray(burn) : span_red(burn))
dat += "<BR>"
if(round(patient.paralysis / 4) >= 1)
dat += text("<HR>Patient paralyzed for: []<BR>", round(patient.paralysis / 4) >= 1 ? "[round(patient.paralysis / 4)] seconds" : "None")
if(patient.getBrainLoss())
dat += "<div class='line'>" + span_orange("Significant brain damage detected.") + "</div><br>"
if(patient.getCloneLoss())
dat += "<div class='line'>" + span_orange("Patient may be improperly cloned.") + "</div><br>"
if(patient)
var/list/ingested_reagents = list()
if(patient.reagents.reagent_list.len)
for(var/datum/reagent/R in patient.reagents.reagent_list)
dat += "<div class='line'><div style='width: 170px;' class='statusLabel'>[R.name]:</div><div class='statusValue'>[round(R.volume, 0.1)] units</div></div><br>"
dat += "</div>"
for(var/datum/reagent/ingested in patient.reagents.reagent_list)
UNTYPED_LIST_ADD(ingested_reagents, list("name" = ingested.name, "volume" = ingested.volume))
var/datum/browser/popup = new(user, "sleeper_b", "[name] Console", 450, 500, src)
popup.set_content(dat)
popup.open()
UI_open = TRUE
return
patient_data = list(
"name" = patient.name,
"stat" = patient.stat,
"pulse" = patient.get_pulse(GETPULSE_TOOL),
"crit_pulse" = (patient.pulse == PULSE_NONE || patient.pulse == PULSE_THREADY),
"health" = patient.health,
"max_health" = patient.getMaxHealth(),
"brute" = patient.getBruteLoss(),
"oxy" = patient.getOxyLoss(),
"tox" = patient.getToxLoss(),
"burn" = patient.getFireLoss(),
"paralysis" = patient.paralysis,
"braindamage" = !!patient.getBrainLoss(),
"clonedamage" = !!patient.getCloneLoss(),
"ingested_reagents" = ingested_reagents
)
/obj/item/dogborg/sleeper/Topic(href, href_list)
if(..() || usr == patient)
return
usr.set_machine(src)
if(href_list["refresh"])
update_patient()
src.updateUsrDialog(usr)
sleeperUI(usr)
return
if(href_list["eject"])
go_out()
sleeperUI(usr)
return
if(href_list["close"])
UI_open = FALSE
return
if(href_list["clean"])
if(!cleaning)
var/confirm = tgui_alert(usr, "You are about to engage self-cleaning mode. This will fill your [src] with caustic enzymes to remove any objects or biomatter, and convert them into energy. Are you sure?", "Confirmation", list("Self-Clean", "Cancel"))
if(confirm == "Self-Clean")
if(cleaning)
return
else
cleaning = 1
drain(startdrain)
START_PROCESSING(SSobj, src)
update_patient()
if(patient)
to_chat(patient, span_danger("[hound.name]'s [src.name] fills with caustic enzymes around you!"))
return
if(cleaning)
sleeperUI(usr)
return
if(href_list["analyze"]) //DO HEALTH ANALYZER STUFF HERE.
med_analyzer.scan_mob(patient,hound)
if(href_list["port"])
switch(eject_port)
if("ingestion")
eject_port = "disposal"
if("disposal")
eject_port = "ingestion"
sleeperUI(usr)
return
if (href_list["ingest"])
vore_ingest_all()
sleeperUI(usr)
return
if(href_list["deliveryslot"])
var/tag = tgui_input_list(usr, "Select active delivery slot:", "Slot Choice", deliverylists)
if(!tag)
return 0
delivery_tag = tag
sleeperUI(usr)
return
if(href_list["slot_eject"])
if(length(deliverylists[delivery_tag]) > 0)
var/list/data = list(
"our_patient" = patient_data,
"eject_port" = eject_port,
"cleaning" = cleaning,
"medsensor" = medsensor,
"delivery" = delivery,
"delivery_tag" = delivery_tag,
"delivery_lists" = deliverylists,
"compactor" = compactor,
"max_item_count" = max_item_count,
"ore_storage" = ore_storage,
"current_capacity" = current_capacity,
"max_ore_storage" = max_ore_storage,
"contents" = contents,
"deliveryslot_1" = deliveryslot_1,
"deliveryslot_2" = deliveryslot_2,
"deliveryslot_3" = deliveryslot_3,
"items_preserved" = items_preserved,
)
return data
/obj/item/dogborg/sleeper/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
if(..())
return TRUE
if(ui.user == patient)
return FALSE
switch(action)
if("eject")
go_out()
return TRUE
if("clean")
if(cleaning)
return FALSE
cleaning = TRUE
drain(startdrain)
START_PROCESSING(SSobj, src)
update_patient()
if(patient)
to_chat(patient, span_danger("[hound.name]'s [src.name] fills with caustic enzymes around you!"))
return TRUE
if("analyze")
med_analyzer.scan_mob(patient,hound)
return TRUE
if("port")
var/new_port = params["value"]
if(!(new_port in list("disposal", "ingestion")))
return FALSE
eject_port = new_port
return TRUE
if("ingest")
vore_ingest_all()
return TRUE
if("deliveryslot")
var/new_tag = params["value"]
if(!(new_tag in deliverylists))
return FALSE
delivery_tag = new_tag
return TRUE
if("slot_eject")
if(!length(deliverylists[delivery_tag]))
return FALSE
hound.visible_message(span_warning("[hound.name] empties out their cargo compartment via their [eject_port] port."), span_notice("You empty your cargo compartment via your [eject_port] port."))
for(var/C in deliverylists[delivery_tag])
if(ishuman(C))
var/mob/living/carbon/human/person = C
person.forceMove(get_turf(src))
else
var/obj/T = C
T.loc = hound.loc
for(var/atom/movable/content in deliverylists[delivery_tag])
content.forceMove(get_turf(src))
playsound(src, 'sound/effects/splat.ogg', 50, 1)
update_patient()
deliverylists[delivery_tag].Cut()
sleeperUI(usr)
return
if(patient && !(patient.stat & DEAD)) //What is bitwise NOT? ... Thought it was tilde.
if(href_list["inject"] == REAGENT_ID_INAPROVALINE || patient.health > min_health)
inject_chem(usr, href_list["inject"])
else
to_chat(usr, span_notice("ERROR: Subject is not in stable condition for injections."))
else
to_chat(usr, span_notice("ERROR: Subject cannot metabolise chemicals."))
updateUsrDialog(usr)
sleeperUI(usr) //Needs a callback to boop the page to refresh.
return
return TRUE
if("inject")
if(!patient || (patient.stat & DEAD))
to_chat(ui.user, span_notice("ERROR: Subject cannot metabolise chemicals."))
return FALSE
var/selected_reagent = params["value"]
if(!(selected_reagent in injection_chems))
return FALSE
if(selected_reagent == REAGENT_ID_INAPROVALINE || patient.health > min_health)
inject_chem(ui.user, selected_reagent)
else
to_chat(ui.user, span_notice("ERROR: Subject is not in stable condition for injections."))
return TRUE
/obj/item/dogborg/sleeper/proc/inject_chem(mob/user, chem)
if(patient && patient.reagents)
@@ -451,8 +382,6 @@
hound = src.loc
if(!istype(hound,/mob/living/silicon/robot))
return
if(UI_open == TRUE)
sleeperUI(hound)
//Cleaning looks better with red on, even with nobody in it
if(cleaning || (length(contents) > 10) || (decompiler && (length(contents) > 5)) || (analyzer && (length(contents) > 1)))

View File

@@ -1681,3 +1681,8 @@
nutrition = 1000
to_chat(src, span_warning("You have purged most of the nutrition lingering in your systems."))
return TRUE
/mob/living/silicon/robot/proc/get_ui_theme()
if(emagged)
return "syndicate"
return ui_theme

View File

@@ -12,7 +12,7 @@ import {
Tabs,
} from 'tgui-core/components';
import { LawManagerLaws, LawManagerLawSets } from '../LawManager';
import { LawManagerLawSets, LawManagerLaws } from '../LawManager';
import { ModifyRobotNoModule } from './ModifyRobotNoModule';
import { ModifyRobotAccess } from './ModifyRobotTabs/ModifyRobotAccess';
import { ModifyRobotComponent } from './ModifyRobotTabs/ModifyRobotComponent';
@@ -63,6 +63,7 @@ export const ModifyRobot = (props) => {
comms_options,
armour_options,
current_gear,
theme,
} = data;
const [tab, setTab] = useState<number>(0);
@@ -146,7 +147,11 @@ export const ModifyRobot = (props) => {
);
return (
<Window width={target?.module ? 900 : 400} height={700}>
<Window
width={target?.module ? 900 : 400}
height={700}
theme={theme || 'ntos'}
>
<Window.Content>
<Stack fill vertical>
<Stack.Item>

View File

@@ -38,6 +38,7 @@ export type Data = {
law_sets: law_pack[];
active_ais: DropdownEntry[];
selected_ai: string | null;
theme?: string;
};
export type DropdownEntry = {

View File

@@ -0,0 +1,32 @@
import { useBackend } from 'tgui/backend';
import { Button, Dropdown, Section, Stack } from 'tgui-core/components';
import type { Data } from '../types';
export const CargoCompartment = (props) => {
const { act, data } = useBackend<Data>();
const { delivery_lists, delivery_tag } = data;
return (
<Section fill title="Cargo Compartment">
<Stack>
<Stack.Item>
<Dropdown
onSelected={(value: string) =>
act('deliveryslot', { value: value })
}
options={Object.keys(delivery_lists)}
selected={delivery_tag}
/>
</Stack.Item>
<Stack.Item>
<Button
disabled={!delivery_lists[delivery_tag]}
onClick={() => act('slot_eject')}
>
Eject Slot
</Button>
</Stack.Item>
</Stack>
</Section>
);
};

View File

@@ -0,0 +1,31 @@
import { useBackend } from 'tgui/backend';
import { Box, Button, Section, Stack } from 'tgui-core/components';
import type { Data, RobotChem } from '../types';
export const InjectorPanel = (props: { robotChems: RobotChem[] }) => {
const { act, data } = useBackend<Data>();
const { our_patient } = data;
const { robotChems } = props;
return (
<Section fill title="Injector">
{robotChems.length ? (
<Stack wrap="wrap">
{robotChems.map((chem) => (
<Stack.Item basis="49%" key={chem.id}>
<Button
fluid
disabled={!our_patient || our_patient.stat === 2}
onClick={() => act('inject', { value: chem.id })}
>
Inject {chem.name}
</Button>
</Stack.Item>
))}
</Stack>
) : (
<Box color="red">No chems found.</Box>
)}
</Section>
);
};

View File

@@ -0,0 +1,19 @@
import { Section, Stack } from 'tgui-core/components';
import { SleeperButtons } from './SleeperStatusElements/SleeperButtons';
import { SleeperStatusPanel } from './SleeperStatusElements/SleeperStatusPanel';
export const SleeperStatus = (props: { name: string }) => {
const { name } = props;
return (
<Section fill title={`${name} Status`}>
<Stack fill vertical>
<SleeperButtons />
<Stack.Divider />
<Stack.Item grow>
<SleeperStatusPanel name={name} />
</Stack.Item>
</Stack>
</Section>
);
};

View File

@@ -0,0 +1,68 @@
import { useBackend } from 'tgui/backend';
import { Box, Button, Dropdown, Stack } from 'tgui-core/components';
import { EJECTION_OPTIONS } from '../../constants';
import type { Data } from '../../types';
export const SleeperButtons = (props) => {
const { act, data } = useBackend<Data>();
const { eject_port, cleaning, medsensor, name } = data;
return (
<>
<Stack.Item>
<Stack align="center">
<Stack.Item>
<Box color="label">Eject Port:</Box>
</Stack.Item>
<Stack.Item>
<Dropdown
onSelected={(value: string) => act('port', { value: value })}
options={EJECTION_OPTIONS}
selected={eject_port}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Stack>
<Stack.Item>
<Button.Confirm
onClick={() => act('eject')}
tooltip="Eject all your sleeper contents."
>
Eject All
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => act('ingest')}
tooltip="Moves all your sleepr contents into your currently selected vorebelly."
>
Vore All
</Button.Confirm>
</Stack.Item>
<Stack.Item>
<Button.Confirm
onClick={() => act('clean')}
disabled={cleaning}
color={cleaning ? 'red' : undefined}
tooltip={`Self-Cleaning mode will fill your ${name} with causic enzymes to remove any objects or biomatter, and convert them into energy.`}
>
Self-Clean
</Button.Confirm>
</Stack.Item>
{!!medsensor && (
<Stack.Item>
<Button
onClick={() => act('analyze')}
tooltip="Scan patient for detailed information."
>
Analyze Patient
</Button>
</Stack.Item>
)}
</Stack>
</Stack.Item>
</>
);
};

View File

@@ -0,0 +1,36 @@
import { Fragment } from 'react';
import { useBackend } from 'tgui/backend';
import { Box, Stack } from 'tgui-core/components';
import { filterFuel, summarizeItems } from '../../functions';
import type { Data } from '../../types';
export const SleeperCargoStatus = (props) => {
const { data } = useBackend<Data>();
const { deliveryslot_1, deliveryslot_2, deliveryslot_3, contents } = data;
const cargoSlots = [deliveryslot_1, deliveryslot_2, deliveryslot_3];
return (
<>
{cargoSlots.map((slot, index) => (
<Fragment key={index}>
<Stack.Item>
<Box color="label">Cargo compartment slot: Cargo {index}.</Box>
</Stack.Item>
<Stack.Item>
<Box color="label">{summarizeItems(slot)}</Box>
</Stack.Item>
</Fragment>
))}
<Stack.Item>
<Box color="red">Cargo compartment slot: Fuel.</Box>
</Stack.Item>
<Stack.Item>
<Box color="red">
{summarizeItems(filterFuel(contents, cargoSlots.flat()))}
</Box>
</Stack.Item>
<Stack.Divider />
</>
);
};

View File

@@ -0,0 +1,92 @@
import { useBackend } from 'tgui/backend';
import { Box, Stack } from 'tgui-core/components';
import { STAT_TO_COLOR } from '../../constants';
import type { Data } from '../../types';
export const SleeperPatient = (props: { name: string }) => {
const { data } = useBackend<Data>();
const { our_patient } = data;
const { name } = props;
if (!our_patient) {
return `${name} Unoccupied.`;
}
const isParalysed = Math.round(our_patient.paralysis / 4) >= 1;
return (
<>
<Stack.Item grow>
<Stack>
<Stack.Item>{our_patient.name}</Stack.Item>
<Stack.Item>{`=>`}</Stack.Item>
<Stack.Item>
<Box color={Object.values(STAT_TO_COLOR)[our_patient.stat]}>
{Object.keys(STAT_TO_COLOR)[our_patient.stat]}
</Box>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.crit_pulse ? 'red' : undefined}>
- Pulse, bpm: {our_patient.pulse}
</Box>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.health > 0 ? undefined : 'red'}>
{`- Overall Health: ${((100 * our_patient.health) / our_patient.max_health).toFixed(0)}`}
</Box>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.brute < 60 ? 'label' : 'red'}>
{`- Brute Damage: ${our_patient.brute.toFixed(0)}`}
</Box>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.oxy < 60 ? 'label' : 'red'}>
{`- Respiratory Damage: ${our_patient.oxy.toFixed(0)}`}
</Box>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.tox < 60 ? 'label' : 'red'}>
{`- Toxin Content: ${our_patient.tox.toFixed(0)}`}
</Box>
</Stack.Item>
<Stack.Item>
<Box color={our_patient.burn < 60 ? 'label' : 'red'}>
{`- Burn Severity: ${our_patient.burn.toFixed(0)}`}
</Box>
</Stack.Item>
{(isParalysed ||
!!our_patient.braindamage ||
!!our_patient.clonedamage ||
!!our_patient.ingested_reagents) && <Stack.Divider />}
{isParalysed && (
<Stack.Item>
<Box bold>
{`Patient paralyzed for: ${Math.round(our_patient.paralysis / 4)} seonds`}
</Box>
</Stack.Item>
)}
{!!our_patient.braindamage && (
<Stack.Item>
<Box color="orange">Significant brain damage detected.</Box>
</Stack.Item>
)}
{!!our_patient.clonedamage && (
<Stack.Item>
<Box color="orange">Patient may be improperly cloned.</Box>
</Stack.Item>
)}
{!!our_patient.ingested_reagents &&
our_patient.ingested_reagents.map((reagent) => (
<Stack.Item key={reagent.name}>
<Box inline preserveWhitespace color="label">
{`${reagent.name}: `}
</Box>
<Box inline>{Math.round(reagent.volume * 10) / 10} units</Box>
</Stack.Item>
))}
</>
);
};

View File

@@ -0,0 +1,45 @@
import { useBackend } from 'tgui/backend';
import { Box, Stack } from 'tgui-core/components';
import { filterFuel } from '../../functions';
import type { Data } from '../../types';
export const SleeperSelfClean = (props) => {
const { data } = useBackend<Data>();
const { cleaning, items_preserved, contents } = data;
const perservedCount = items_preserved.length;
const cleanableCount = filterFuel(contents, items_preserved).length;
return (
<>
{!!cleaning && (
<Stack.Item>
{cleanableCount ? (
<>
<Box color="red" inline>
Self-cleaning mode.
</Box>
<Box inline preserveWhitespace>
{`${cleanableCount} object${cleanableCount > 1 ? 's' : ''} remaining.`}
</Box>
</>
) : (
!!items_preserved && (
<Box color="red">
Self-cleaning done. Eject remaining objects now.
</Box>
)
)}
</Stack.Item>
)}
{!!perservedCount && (
<Stack.Item>
<Box color="red">
{`${perservedCount} uncleanable object${perservedCount > 1 ? 's' : ''}.`}
</Box>
</Stack.Item>
)}
{!!cleaning || (!!perservedCount && <Stack.Divider />)}
</>
);
};

View File

@@ -0,0 +1,71 @@
import { useBackend } from 'tgui/backend';
import { Box, Section, Stack } from 'tgui-core/components';
import { currentLoadToColor, summarizeItems } from '../../functions';
import type { Data } from '../../types';
import { SleeperCargoStatus } from './SleeperCargoStatus';
import { SleeperPatient } from './SleeperPatient';
import { SleeperSelfClean } from './SleeperSelfClean';
export const SleeperStatusPanel = (props: { name: string }) => {
const { data } = useBackend<Data>();
const {
delivery,
compactor,
contents,
max_item_count,
ore_storage,
current_capacity,
max_ore_storage,
} = data;
const { name } = props;
return (
<Section fill scrollable>
<Stack fill vertical>
{(!!delivery || !!compactor) && !!contents.length && (
<>
<Stack.Item>
<Box bold inline>
Current load:
</Box>
<Box
inline
preserveWhitespace
color={currentLoadToColor(contents.length, max_item_count)}
>
{` ${contents.length} / ${max_item_count} objects.`}
</Box>
</Stack.Item>
{delivery ? (
<SleeperCargoStatus />
) : (
<Stack.Item>
<Box color="label">{summarizeItems(contents)}</Box>
</Stack.Item>
)}
<Stack.Divider />
</>
)}
{!!ore_storage && (
<>
<Stack.Item>
<Box bold inline>
Current ore capacity:
</Box>
<Box
inline
preserveWhitespace
color={currentLoadToColor(current_capacity, max_ore_storage)}
>
{` ${current_capacity} / ${max_ore_storage} ores.`}
</Box>
</Stack.Item>
<Stack.Divider />
</>
)}
<SleeperSelfClean />
<SleeperPatient name={name} />
</Stack>
</Section>
);
};

View File

@@ -0,0 +1,6 @@
export const EJECTION_OPTIONS = ['disposal', 'ingestion'];
export const STAT_TO_COLOR = {
Conscious: 'green',
Unconscious: 'orange',
DEAD: 'red',
};

View File

@@ -0,0 +1,38 @@
export function currentLoadToColor(
current: number,
maximum: number,
): string | undefined {
const fillState = current / maximum;
if (fillState < 0.75) {
return undefined;
}
if (fillState < 1) {
return 'yellow';
}
return 'red';
}
export function summarizeItems(items: string[]): string {
const countMap = items.reduce<Record<string, number>>((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {});
return [...new Set(items)]
.map((item) => (countMap[item] > 1 ? `${item} x${countMap[item]}` : item))
.join(', ');
}
export function filterFuel(contents: string[], cargo: string[]): string[] {
const cargoCopy = [...cargo];
return contents.reduce<string[]>((acc, item) => {
const index = cargoCopy.indexOf(item);
if (index !== -1) {
cargoCopy.splice(index, 1);
} else {
acc.push(item);
}
return acc;
}, []);
}

View File

@@ -0,0 +1,34 @@
import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts';
import { Stack } from 'tgui-core/components';
import { CargoCompartment } from './SubElements/CargoCompartment';
import { InjectorPanel } from './SubElements/InjectorPanel';
import { SleeperStatus } from './SubElements/SleeperStatus';
import type { Data } from './types';
export const RobotSleeper = (props) => {
const { data } = useBackend<Data>();
const { theme, chems, delivery, name } = data;
return (
<Window width={450} height={500} theme={theme || 'ntos'}>
<Window.Content>
<Stack vertical fill>
{!!chems && (
<Stack.Item>
<InjectorPanel robotChems={chems} />
</Stack.Item>
)}
{!!delivery && (
<Stack.Item>
<CargoCompartment />
</Stack.Item>
)}
<Stack.Item grow>
<SleeperStatus name={name ?? 'Unknown'} />
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};

View File

@@ -0,0 +1,44 @@
import type { BooleanLike } from 'tgui-core/react';
export type Data = {
name?: string;
theme?: string;
chems?: RobotChem[];
our_patient: Patient | null;
eject_port: string;
cleaning: BooleanLike;
medsensor: BooleanLike;
delivery: BooleanLike;
delivery_tag: string;
delivery_lists: Record<string, string[]>;
compactor: BooleanLike;
max_item_count: number;
ore_storage: BooleanLike;
current_capacity: number;
max_ore_storage: number;
contents: string[];
deliveryslot_1: string[];
deliveryslot_2: string[];
deliveryslot_3: string[];
items_preserved: string[];
};
export type RobotChem = { id: string; name: string };
export type Patient = {
name: string;
stat: number;
pulse: number;
crit_pulse: BooleanLike;
health: number;
max_health: number;
brute: number;
oxy: number;
tox: number;
burn: number;
paralysis: number;
braindamage: BooleanLike;
clonedamage: BooleanLike;
ingested_reagents: { name: string; volume: number }[];
};