import type { CardProps } from "@thorium/cards/CardProps"; import { systemCategories, systemFilterValues, systemPowerPriority, } from "@thorium/cards/DamageReports/systemCategories"; import { q } from "@thorium/context/AppContext"; import { useCardContext } from "@thorium/context/CardContext"; import useAnimationFrame from "@thorium/hooks/useAnimationFrame"; import useEventListener from "@thorium/hooks/useEventListener"; import { useStation } from "@thorium/routes/station/useStation"; import Button from "@thorium/ui/Button"; import { popoverTransitionClasses } from "@thorium/ui/Dropdown"; import { HoldButton } from "@thorium/ui/HoldButton"; import { Icon } from "@thorium/ui/Icon"; import RadialDial from "@thorium/ui/RadialDial"; import { SelectItem } from "@thorium/ui/Select"; import { Tooltip } from "@thorium/ui/Tooltip"; import { cn } from "@thorium/utils/cn"; import { useLiveQuery } from "@thorium/utils/live-query/client"; import { useImperativeHandle, useRef, useState, type Ref } from "react"; import { Button as RAButton, ListBox, Popover, Select as RASelect, Label, } from "react-aria-components"; export function SystemsMonitor({ cardLoaded }: CardProps) { const { shipId } = useStation(); const [reactors] = q.systemsMonitor.reactors.get.useNetRequest( { shipId }, // Refetch to refresh the active and reserve fuel amount { refetchInterval: 1000 }, ); const [batteries] = q.systemsMonitor.batteries.get.useNetRequest({ shipId }); const [systems] = q.systemsMonitor.systems.get.useNetRequest({ shipId }); const maxSystemPower = systems.reduce( (prev, next) => typeof next.power?.powerLevels.at(-1) === "undefined" || prev > next.power.powerLevels.at(-1)! ? prev : next.power.powerLevels.at(-1)!, 0, ); q.systemsMonitor.stream.useDataStream({ shipId }); /** * Sort systems by * - Name * - Category * - Highest max power draw * - Activated/Deactivated */ const [selectedSort, setSelectedSort] = useState< "name" | "category" | "draw+" | "draw-" | "priority" >("category"); const [powerDrawOrder, setPowerDrawOrder] = useState([]); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { if (selectedSort === "category") { const newDrawOrder = systems .map((sys) => ({ id: sys.id, draw: interpolate(sys.id)?.y || 0 })) .sort((a, b) => b.draw - a.draw) .map((m) => m.id); if (newDrawOrder.join(",") !== powerDrawOrder.join(",")) setPowerDrawOrder(newDrawOrder); } }, cardLoaded); return (
{reactors.map((reactor, i) => ( ))}
{batteries.map((battery, i) => ( ))}
{systems .concat() .sort((a, b) => { switch (selectedSort) { case "category": return ( systemFilterValues.indexOf(systemCategories[a.type]) - systemFilterValues.indexOf(systemCategories[b.type]) ); case "name": return a.name.localeCompare(b.name); case "priority": return systemPowerPriority[a.type] - systemPowerPriority[b.type]; case "draw+": return powerDrawOrder.indexOf(a.id) - powerDrawOrder.indexOf(b.id); case "draw-": return powerDrawOrder.indexOf(b.id) - powerDrawOrder.indexOf(a.id); default: return 0; } }) .map((system) => ( ))}

Sort:

); } function Reactor({ name, id, index, nominalHeat, maxSafeHeat, maxHeat, fuel, reserve, desiredOutput, maxOutput, optimalOutputPercent, efficiency, cardLoaded, }: { name: string; id: number; index: number; nominalHeat: number; maxSafeHeat: number; maxHeat: number; fuel: number; reserve: number; desiredOutput: number; maxOutput: number; optimalOutputPercent: number; efficiency?: number; cardLoaded: boolean; }) { const heatRef = useRef(null); const heatProgressRef = useRef<{ setValue: (value: number) => void }>(null); const powerProgressRef = useRef(null); const powerRef = useRef(null); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { const reactor = interpolate(id); if (!reactor) return; if (powerProgressRef.current) powerProgressRef.current.style.width = `${(reactor.x / maxOutput) * 100}%`; if (powerRef.current) powerRef.current.textContent = `Current Output: ${Math.round(reactor.x * 10) / 10} MW`; const heat = reactor.z; const heatValue = (heat - nominalHeat) / (maxHeat - nominalHeat); if (heatRef.current) { heatRef.current.innerText = `Heat: ${Math.round(heat)}K`; } if (heatProgressRef.current) { heatProgressRef.current.setValue(heatValue); } }, cardLoaded); return (
{name} {index + 1}
{typeof efficiency === "number" ? ( ) : null}
{ q.systemsMonitor.reactors.setDesiredOutput.netSend({ reactorId: id, output: desiredOutput - actionCount, }); }} >
{ q.systemsMonitor.reactors.setDesiredOutput.netSend({ reactorId: id, output: desiredOutput + actionCount, }); }} >

{desiredOutput} / {maxOutput}{" "} MW

); } function ReactorSummary() { const { shipId } = useStation(); const { cardLoaded } = useCardContext(); const [reactors] = q.systemsMonitor.reactors.get.useNetRequest({ shipId }); const [systems] = q.systemsMonitor.systems.get.useNetRequest({ shipId }); const [batteries] = q.systemsMonitor.batteries.get.useNetRequest({ shipId }); const outputRef = useRef(null); const netOutputRef = useRef(null); const availableRef = useRef(null); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { let output = 0; for (const reactor of reactors) { const reactorData = interpolate(reactor.id); if (!reactorData) continue; output += reactorData.x; } if (outputRef.current) outputRef.current.textContent = `${Math.round(output * 10) / 10}`; let used = 0; for (const system of systems) { const systemData = interpolate(system.id); if (!systemData || typeof systemData.y !== "number" || Number.isNaN(systemData.y)) continue; used += systemData.y; } let batteryOutput = 0; let batteryInput = 0; for (const battery of batteries) { const batteryData = interpolate(battery.id); if (!batteryData) continue; batteryOutput += batteryData.z; batteryInput += batteryData.y; } used += batteryInput; if (netOutputRef.current) netOutputRef.current.textContent = `${Math.round((used - batteryOutput) * 10) / 10}`; if (availableRef.current) availableRef.current.textContent = `${Math.round((output - used - batteryInput) * 10) / 10}`; }, cardLoaded); return (

Total Output: MW

Net Output: MW

Total Available: MW

); } function BatterySummary() { const { shipId } = useStation(); const { cardLoaded } = useCardContext(); const [batteries] = q.systemsMonitor.batteries.get.useNetRequest({ shipId }); const inputRef = useRef(null); const outputRef = useRef(null); const chargeRef = useRef(null); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { let batteryOutput = 0; let batteryInput = 0; for (const battery of batteries) { const batteryData = interpolate(battery.id); if (!batteryData) continue; batteryInput += batteryData.y; batteryOutput += batteryData.z; } if (outputRef.current) outputRef.current.textContent = `${Math.round(batteryOutput * 10) / 10}`; if (inputRef.current) inputRef.current.textContent = `${Math.round(batteryInput * 10) / 10}`; if (chargeRef.current) chargeRef.current.textContent = `${Math.round((batteryInput - batteryOutput) * 10) / 10}`; }, cardLoaded); return (

Battery Input: MW

Battery Output: MW

Net Change: MW

); } class HoverBatteryEvent extends Event { static name = "hover-battery-event"; constructor(public batteryId: number | null) { super(HoverBatteryEvent.name); } } class HoverSystemsEvent extends Event { static name = "hover-systems-event"; constructor(public batteryId: number | null) { super(HoverSystemsEvent.name); } } function Battery({ id, index, name, type, outputRate, capacity, activated, }: { id: number; index: number; type: string; name: string; outputRate: number; capacity: number; activated: boolean; }) { const { cardLoaded } = useCardContext(); const storageRef = useRef(null); const storageNumberRef = useRef(null); const storageProgressRef = useRef(null); const outputRef = useRef(null); const outputProgressRef = useRef(null); const outputNumberRef = useRef(null); const chargingRef = useRef(null); const equalRef = useRef(null); const dischargingRef = useRef(null); const chargeLabelRef = useRef(null); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { const system = interpolate(id); if (!system) return; const storage = system.x; const chargeAmount = system.y; const dischargeAmount = system.z; if (storageRef.current) { storageRef.current.innerText = `Storage: ${((storage / capacity) * 100).toFixed( 0, )}% (${storage.toFixed(2)} MWh)`; } if (storageNumberRef.current) { storageNumberRef.current.textContent = `${Math.round(storage * 10) / 10}`; } if (storageProgressRef.current) { storageProgressRef.current.style.width = `${(storage / capacity) * 100}%`; } if (outputRef.current) { outputRef.current.innerText = `Output: ${Math.round(dischargeAmount * 10) / 10} MW`; } if (outputProgressRef.current) { outputProgressRef.current.style.width = `${(dischargeAmount / outputRate) * 100}%`; } if (outputNumberRef.current) { outputNumberRef.current.textContent = `${Math.round(dischargeAmount * 10) / 10}`; } if (chargeAmount > dischargeAmount) { chargingRef.current?.classList.remove("hidden"); dischargingRef.current?.classList.add("hidden"); equalRef.current?.classList.add("hidden"); } if (chargeAmount < dischargeAmount) { chargingRef.current?.classList.add("hidden"); dischargingRef.current?.classList.remove("hidden"); equalRef.current?.classList.add("hidden"); } if (chargeAmount === dischargeAmount) { chargingRef.current?.classList.add("hidden"); dischargingRef.current?.classList.add("hidden"); equalRef.current?.classList.remove("hidden"); } if (chargeLabelRef.current) { chargeLabelRef.current.textContent = `Net Change: ${Math.round((chargeAmount - dischargeAmount) * 10) / 10} MW`; } }, cardLoaded); const [hovered, setHovered] = useState(false); useEventListener(HoverBatteryEvent.name, (event: HoverBatteryEvent) => { if (event.batteryId === id) setHovered(true); else setHovered(false); }); return (
{ window.dispatchEvent(new HoverSystemsEvent(id)); }} onPointerLeave={() => { window.dispatchEvent(new HoverSystemsEvent(null)); }} > {name}
{" "} {activated ? ( ) : ( )}
{/* */}

/{" "} {capacity} MWh

/{" "} {outputRate} MW

); } function System({ id, name, type, power, heat, max, }: { id: number; name: string; type: string; power?: { powerLevels: number[]; batterySource: number | null; activated: boolean }; heat?: { heat: number; maxSafeHeat: number; maxHeat: number; nominalHeat: number; }; max: number; }) { const { shipId } = useStation(); const { cardLoaded } = useCardContext(); const [batteries] = q.systemsMonitor.batteries.get.useNetRequest({ shipId }); const heatRef = useRef(null); const heatProgressRef = useRef<{ setValue: (value: number) => void }>(null); const powerRef = useRef(null); const powerProgressRef = useRef(null); const powerNumberRef = useRef(null); const batteryRef = useRef<{ setPercentage: (value: number) => void; }>(null); const connectedBattery = batteries.find((b) => b.id === power?.batterySource); const [hovered, setHovered] = useState(false); useEventListener(HoverSystemsEvent.name, (event: HoverSystemsEvent) => { if (power?.batterySource && event.batteryId === power?.batterySource) setHovered(true); else setHovered(false); }); const { interpolate } = useLiveQuery(); useAnimationFrame(() => { const system = interpolate(id); if (!system) return; const currentPower = system.y; const heatValue = system.z; if (heatRef.current) { heatRef.current.innerText = `Heat: ${Math.round(heatValue)}K`; } if (heatProgressRef.current && heat) { heatProgressRef.current.setValue( (heatValue - heat.nominalHeat) / (heat.maxHeat - heat.nominalHeat), ); } if (power) { if (powerRef.current) powerRef.current.textContent = `Power Draw: ${Math.round(currentPower * 100) / 100} MW`; if (powerProgressRef.current) powerProgressRef.current.style.width = `${(currentPower / max) * 100}%`; if (powerNumberRef.current) powerNumberRef.current.textContent = `${Math.round(currentPower * 10) / 10}`; if (connectedBattery) { batteryRef.current?.setPercentage(connectedBattery.storage / connectedBattery.capacity); } } }, cardLoaded); if (!power && !heat) return null; return (
{name} {heat ? ( ) : null} {power ? ( <> typeof value !== "string" && q.systemsMonitor.systems.setBatterySource.netSend({ systemId: id, batterySource: value, }) } > { if (connectedBattery) window.dispatchEvent(new HoverBatteryEvent(connectedBattery.id)); }} onPointerLeave={() => { window.dispatchEvent(new HoverBatteryEvent(null)); }} > {connectedBattery ? ( ) : ( )} {batteries.map((b) => ( ))} {power.activated ? ( ) : ( )} ) : null}
{power ? (
{power.powerLevels.map((l, i, arr) => i === arr.length - 1 ? null : (
), )}

/{" "} {power.powerLevels.at(-1)} MW

) : null}
); } function BatteryIcon({ percentage, ref, }: { percentage: number; ref: Ref<{ setPercentage: (value: number) => void }>; }) { const batteryRef100 = useRef(null); const batteryRef75 = useRef(null); const batteryRef50 = useRef(null); const batteryRef25 = useRef(null); const batteryRef0 = useRef(null); useImperativeHandle( ref, () => ({ setPercentage: (percentage: number) => { batteryRef100.current?.classList.add("hidden"); batteryRef75.current?.classList.add("hidden"); batteryRef50.current?.classList.add("hidden"); batteryRef25.current?.classList.add("hidden"); batteryRef0.current?.classList.add("hidden"); switch (true) { case percentage > 0.95: batteryRef100.current?.classList.remove("hidden"); break; case percentage > 0.6: batteryRef75.current?.classList.remove("hidden"); break; case percentage > 0.4: batteryRef50.current?.classList.remove("hidden"); break; case percentage > 0.1: batteryRef25.current?.classList.remove("hidden"); break; default: batteryRef0.current?.classList.remove("hidden"); } }, }), [], ); return ( <>
= 0.95 || percentage < 0.6 ? "hidden" : ""}>
= 0.6 || percentage < 0.4 ? "hidden" : ""}>
= 0.4 || percentage < 0.1 ? "hidden" : ""}>
= 0.1 ? "hidden" : ""}>
); }