Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
11 kB · 406 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407import { keepPreviousData } from "@tanstack/react-query";import { lrmStateMap } from "@thorium/cards/LongRangeComm/shared";import { q } from "@thorium/context/AppContext";import { SelectStarmapEntityEvent } from "@thorium/cores/StarmapCore";import { pickStarmapShip } from "@thorium/cores/StarmapCore/pickShip";import { useStation } from "@thorium/routes/station/useStation";import Button from "@thorium/ui/Button";import { InputField, OutputField, TypingField } from "@thorium/ui/Core";import InfoTip from "@thorium/ui/InfoTip";import Select from "@thorium/ui/Select";import { cn } from "@thorium/utils/cn";import { Suspense, startTransition, useState } from "react";
export function LongRangeCommMessagesCore() { const { shipId } = useStation(); const [outgoingMessages] = q.longRangeComm.outgoingMessages.useNetRequest({ shipId, filter: "all", }); const [selectedMessageId, setSelectedMessageId] = useState<null | number>(null); const selectedMessage = outgoingMessages.find((o) => o.id === selectedMessageId);
return ( <div className="flex h-full flex-auto"> <ul className="max-w-56 overflow-y-auto rounded border border-white/30"> {outgoingMessages.map((message) => ( <li key={message.id} className={cn("list-group-item list-group-item-xs", { selected: selectedMessageId === message.id, })} onClick={() => setSelectedMessageId(message.id)} > {message.destinationShipName} <small className="block"> {new Date(message.timestamp).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", })} </small> </li> ))} </ul>
<div className="flex flex-auto flex-col text-sm"> {selectedMessage && ( <> <div> Sender:{" "} <OutputField className="ml-1 inline px-2"> {selectedMessage.senderStation} </OutputField> </div> <div> State: <OutputField className="ml-1 inline px-2"> {lrmStateMap[selectedMessage.state]} </OutputField> </div> <div> Destination: <OutputField className="ml-1 inline px-2"> {selectedMessage.destinationShipName} </OutputField> </div> <div>Message:</div> <TypingField readOnly className="w-full flex-auto overflow-y-auto px-2 text-left"> {selectedMessage.message} </TypingField> </> )} </div> </div> );}
export function LongRangeCommComposerCore() { const { shipId } = useStation();
const [senderId, setSenderId] = useState<number>(); const [senderStation, setSenderStation] = useState(""); const [addressBookName, setAddressBookName] = useState(""); const [destinationId, setDestinationId] = useState<number>(); const [message, setMessage] = useState(""); const [encoding, setEncoding] = useState<"decoded" | "waves" | "replacement" | "rotation">( "decoded", );
return ( <div className="flex h-full flex-col text-sm"> <Suspense> <SenderInput destinationId={destinationId} senderId={senderId} setSenderId={setSenderId} addressBookName={addressBookName} setAddressBookName={setAddressBookName} /> </Suspense> {senderId === shipId && ( <> <p className="text-xs">Sender Station</p> <InputField className="text-xs" onClick={(val) => setSenderStation(val)} prompt="Which station or person on the ship is sending the message out?" > {senderStation} </InputField> </> )}
<Suspense> <DestinationInput destinationId={destinationId} setDestinationId={setDestinationId} senderId={senderId} /> </Suspense>
<TypingField className="w-full flex-auto px-2 text-left text-sm" value={message} onChange={(e) => setMessage(e.currentTarget.value)} /> <div className="flex flex-wrap items-center gap-1"> <Button className="btn-xs btn-error flex-auto" onClick={() => { setSenderId(undefined); setDestinationId(undefined); setSenderStation(""); setAddressBookName(""); setMessage(""); }} > Clear </Button>{" "} <Select items={[ { id: "decoded", label: "Decoded" }, { id: "waves", label: "Waves" }, { id: "replacement", label: "Replacement" }, { id: "rotation", label: "Rotation" }, ]} label="Encoding" labelHidden size="xs" selected={encoding} setSelected={(value) => setEncoding(value || "decoded")} /> <Button className="btn-xs btn-info flex-auto" disabled={ (senderId === shipId && !senderStation) || !destinationId || !senderId || !message.trim() } onClick={() => { if ((senderId === shipId && !senderStation) || !destinationId || !senderId || !message) return; q.longRangeComm.composeMessage.netSend({ senderId, destinationId, message, senderStation, state: "pending", encoding, }); setSenderId(undefined); setDestinationId(undefined); setSenderStation(""); setAddressBookName(""); setMessage(""); }} > Queue </Button> <Button className="btn-xs btn-success flex-auto" disabled={ (senderId === shipId && !senderStation) || !destinationId || !senderId || !message.trim() } onClick={() => { if ((senderId === shipId && !senderStation) || !destinationId || !senderId || !message) return; q.longRangeComm.composeMessage.netSend({ senderId, destinationId, message, senderStation, state: "sending", encoding, }); setSenderId(undefined); setDestinationId(undefined); setSenderStation(""); setAddressBookName(""); setMessage(""); }} > Send Now </Button> </div> </div> );}
function SenderInput({ destinationId, senderId, setSenderId, addressBookName, setAddressBookName,}: { destinationId: number | undefined; senderId: number | undefined; setSenderId: (value: number) => void; addressBookName: string; setAddressBookName: (value: string) => void;}) { const { shipId } = useStation();
const [addressBook] = q.longRangeComm.addressBook.useNetRequest( { shipId: destinationId || shipId, }, { placeholderData: keepPreviousData, }, ); const [senderObject] = q.starmapCore.object.useNetRequest({ objectId: senderId, }); const addressBookEntry = addressBook.find((a) => a.id === senderId);
const isNewAddressBookEntry = !!senderId && !addressBookEntry && senderId !== shipId && destinationId === shipId;
return ( <> <p className="text-xs">Sender</p> <div className="flex"> {/* TODO March 10, 2026 - This should support both picking from the starmap AND searching through the ship's address book */} <OutputField className="flex-auto" alert={isNewAddressBookEntry} title={isNewAddressBookEntry ? "Sender will be added to the ship's address book" : ""} > {senderId ? addressBookEntry?.name || senderObject?.components.identity?.name || `Entity ID ${senderId}` : ""} {senderId && addressBookEntry?.name ? ` (${senderObject?.components.identity?.name})` : ""} </OutputField> <Button className={cn("btn-xs btn-info", { "rounded-r-none": destinationId !== shipId, })} onClick={() => { pickStarmapShip("Choose a ship to send the long range message.", (senderId) => { startTransition(() => { setSenderId(senderId); }); }); }} > Pick from Starmap </Button> {destinationId !== shipId && ( <Button className="btn-xs btn-success rounded-l-none" onClick={() => setSenderId(shipId)}> Player Ship </Button> )} </div> {isNewAddressBookEntry && ( <> <p className="text-xs"> Address Book Name{" "} <InfoTip> Name used for this sender when added to the ship's address book. Leave blank to use the sender's actual name. </InfoTip> </p> <InputField onClick={(val) => setAddressBookName(val)} prompt="What name should this sender have when added to the ship's address book?" > {addressBookName} </InputField> </> )} </> );}function DestinationInput({ destinationId, setDestinationId, senderId,}: { senderId: number | undefined; destinationId: number | undefined; setDestinationId: (value: number) => void;}) { const { shipId } = useStation();
const [destinationObject] = q.starmapCore.object.useNetRequest( { objectId: destinationId }, { placeholderData: keepPreviousData }, );
const [addressBook] = q.longRangeComm.addressBook.useNetRequest( { shipId: senderId || shipId, }, { placeholderData: keepPreviousData, }, ); const addressBookEntry = addressBook.find((a) => a.id === destinationId);
return ( <> <p className="text-xs">Destination</p> <div className="flex items-center"> <OutputField className="flex-auto"> {destinationId ? addressBookEntry?.name || destinationObject?.components.identity?.name || `Entity ID ${senderId}` : ""} </OutputField> <Button className={cn("btn-xs btn-info", { "rounded-r-none": senderId !== shipId, })} onClick={() => { pickStarmapShip("Choose a ship to receive the long range message.", (destinationId) => { startTransition(() => { setDestinationId(destinationId); }); }); }} > Pick from Starmap </Button> {senderId !== shipId && ( <Button className="btn-xs btn-success rounded-l-none" onClick={() => setDestinationId(shipId)} > Player Ship </Button> )} </div> </> );}
export function LongRangeCommAddressBookCore() { const { shipId } = useStation(); const [addressBook] = q.longRangeComm.addressBook.useNetRequest({ shipId }); const [selectedContact, setSelectedContact] = useState<number | null>(null); return ( <div className="flex h-full flex-col"> <ul className="flex-auto overflow-y-auto rounded border border-white/30"> {addressBook.map((contact) => ( <li key={contact.id} className={cn("list-group-item list-group-item-xs", { selected: selectedContact === contact.id, })} onClick={() => { setSelectedContact(contact.id); window.dispatchEvent(new SelectStarmapEntityEvent(contact.id)); }} > {contact.name} {contact.name ? ` (${contact.entityName})` : contact.entityName} </li> ))} </ul> {selectedContact ? ( <Button className="btn-xs btn-error" onClick={() => { q.longRangeComm.removeFromAddressBook.netSend({ shipId, contactId: selectedContact, }); setSelectedContact(null); }} > Delete Contact </Button> ) : null} </div> );}