Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
15 kB · 570 lines
TSX
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571import { isSortable } from "@dnd-kit/react/sortable";import type { TimelineStep } from "@thorium/.server/classes/Plugins/TimelineStep";import { AddBlockButton } from "@thorium/components/timelineBuilder/AddBlockMenu";import { DefinedVariableProvider, useDefinedVariables,} from "@thorium/components/timelineBuilder/DefinedVariableContext";import { SortableBlocks } from "@thorium/components/timelineBuilder/SortableBlocks";import type { TimelineBlock } from "@thorium/components/timelineBuilder/TimelineBlockTypes";import { q } from "@thorium/context/AppContext";import { toast } from "@thorium/context/ToastContext";import { InterpolateInfo } from "@thorium/routes/config/reports/InterpolateInfo";import { reportVariableNames } from "@thorium/routes/config/reports/reportAvailableVariables";import { trainingVariableNames } from "@thorium/routes/config/trainings/trainingAvailableVariables";import { usePrompt } from "@thorium/ui/AlertDialog";import Button from "@thorium/ui/Button";import InfoTip from "@thorium/ui/InfoTip";import Input from "@thorium/ui/Input";import { SortableList } from "@thorium/ui/SortableItem";import TagInput from "@thorium/ui/TagInput";import { parseSchema } from "@thorium/utils/zodAutoForm";import { parseSchema as parseJsonSchema } from "json-schema-to-zod";import { Suspense } from "react";import { Button as RAButton } from "react-aria-components";
export function TimelineStepEditor({ pluginId, timelineId, timelineType, step,}: { pluginId: string; timelineId: string; timelineType: "reports" | "missions" | "trainings"; step: TimelineStep;}) { const stepId = step.id; const variables = timelineType === "reports" ? reportVariableNames : timelineType === "trainings" ? trainingVariableNames : []; const getActionPresetValues = useGetActionPresetValue(timelineType); return ( <div className="flex flex-1 flex-col"> <div className="flex w-full justify-between gap-2"> <div className="flex-1"> <Input labelHidden={false} label="Step Name" placeholder="Retrieve Information" key={step.id} defaultValue={step.name} className="input-xs" labelProps={{ className: "text-xs" }} onBlur={async (e: any) => { try { await q.plugin.timeline.step.update.netSend({ pluginId, timelineId, timelineType, stepId, name: e.target.value, }); } catch (err) { if (err instanceof Error) { toast({ title: "Error renaming step", body: err.message, color: "error", }); } } }} /> <TagInput className="input-xs flex-1" inputClassName="input-xs" label="Tags" tags={step.tags} onAdd={(tag) => { if (step.tags.includes(tag)) return; q.plugin.timeline.step.update.netSend({ pluginId, timelineId, timelineType, stepId, tags: [...step.tags, tag], }); }} onRemove={(tag) => { if (!step.tags.includes(tag)) return; q.plugin.timeline.step.update.netSend({ pluginId, timelineId, timelineType, stepId, tags: step.tags.filter((t) => t !== tag), }); }} /> </div> <div className="flex-1"> <Input as="textarea" className="input-xs" labelProps={{ className: "text-xs" }} labelHidden={false} label={ timelineType === "reports" ? ( <> Instructions <InterpolateInfo className="static" /> </> ) : ( "Description" ) } helperText={ timelineType === "reports" ? ( <> The instructions that will be displayed to the crew in the damage report for this step. You can use variables from your blocks in this field. </> ) : null } key={step.id} defaultValue={step.description} onBlur={(e: any) => q.plugin.timeline.step.update.netSend({ pluginId, timelineId, timelineType, stepId, description: e.target.value, }) } /> </div> </div> <h3 className="font-semibold"> Blocks{" "} <InfoTip> <p> Compose blocks together to create the logic for your timeline step. Get entity references, store properties in variables, and execute actions. </p> <p>The following variables are available:</p> <ul className="ml-4 list-disc"> {variables.map((a) => ( <li key={a}>{a}</li> ))} </ul> </InfoTip> </h3> <DefinedVariableProvider variables={[...variables]}> <Suspense> <div className="flex-1 overflow-x-hidden overflow-y-auto text-sm"> {!step?.blocks || step?.blocks?.length === 0 ? ( <div> <p>No blocks added to step.</p> <AddBlockButton executionType={["main"]} timelineType={timelineType} onAddBlock={async (type, init, initOverrides) => { if ( type === "Action" && init && "action" in init && typeof init.action === "string" ) { // @ts-expect-error init.values = getActionPresetValues(init.action, init.values, initOverrides); }
await q.plugin.timeline.step.block.add.netSend({ pluginId, timelineId, timelineType, stepId, blockType: type, init, }); }} > <RAButton className="btn btn-sm btn-outline btn-success">Add Block</RAButton> </AddBlockButton> </div> ) : ( <SortableBlocks timelineType={timelineType} executionType={["main"]} blocks={step?.blocks || []} onDragEnd={(event) => { if ( event.canceled || !event.operation.source || !isSortable(event.operation.source) ) return; q.plugin.timeline.step.block.reorder.netSend({ pluginId, timelineId, timelineType, stepId, blockId: event.operation.source.id as string, newIndex: event.operation.source.index, }); }} onUpdate={async (block, property, value) => { await q.plugin.timeline.step.block.update.netSend({ pluginId, timelineId, timelineType, stepId, blockId: block.id, properties: { [property]: value }, }); }} onReplace={(id, blocks) => { q.plugin.timeline.step.block.replace.netSend({ pluginId, timelineId, timelineType, stepId, blockId: id, blocks, }); }} onRemove={(id) => q.plugin.timeline.step.block.delete.netSend({ pluginId, timelineId, timelineType, stepId, blockId: id, }) } /> )} </div> </Suspense>
<AddBlockButton timelineType={timelineType} executionType={["main"]} onAddBlock={async (type, init, initOverrides) => { if (type === "Action" && init && "action" in init && typeof init.action === "string") { // @ts-expect-error init.values = getActionPresetValues(init.action, init.values, initOverrides); } await q.plugin.timeline.step.block.add.netSend({ pluginId, timelineId, timelineType, stepId, blockType: type, init, }); }} > <RAButton className="btn btn-sm btn-outline btn-success">Add Block</RAButton> </AddBlockButton> </DefinedVariableProvider> </div> );}
export function StepButtons({ pluginId, timelineId, timelineType, stepId, setStep,}: { pluginId: string; timelineId: string; stepId: string | undefined | null; timelineType: "reports" | "missions" | "trainings"; setStep: (stepId: string | null) => void;}) { const prompt = usePrompt();
return ( <div className="mb-2 flex"> <Button className="btn-xs btn-success grow" onClick={async () => { const name = await prompt("What is the new step name?"); if (!name) return; const step = await q.plugin.timeline.step.add.netSend({ pluginId, timelineId, timelineType, name, }); setStep(`${step.stepId}`); }} > Add Step </Button> <Button className="btn-xs btn-warning grow" disabled={!stepId} onClick={async () => { const name = await prompt("What is the new step name?"); if (!name || !stepId) return; const step = await q.plugin.timeline.step.insert.netSend({ pluginId, timelineId, timelineType, stepId, name, }); setStep(`${step.stepId}`); }} > Insert Step </Button> <Button className="btn-xs btn-info grow" disabled={!stepId} onClick={async () => { if (!stepId) return; const step = await q.plugin.timeline.step.duplicate.netSend({ pluginId, timelineId, timelineType, stepId, }); setStep(`${step.stepId}`); }} > Duplicate </Button> <Button className="btn-xs btn-error grow" disabled={!stepId} onClick={async () => { if (!stepId) return; const { alternateStep } = await q.plugin.timeline.step.delete.netSend({ pluginId, timelineId, timelineType, stepId, }); setStep(alternateStep); }} > Delete </Button> </div> );}
export function StepList({ pluginId, timelineId, timelineType, stepId, setStep,}: { pluginId: string; timelineId: string; timelineType: "reports" | "missions" | "trainings";
stepId?: string | null; setStep: (id: string) => void;}) { const [item] = q.plugin.timeline.get.useNetRequest({ pluginId, timelineId, timelineType, });
const steps = item.steps.map((s) => ({ id: s.id, children: s.name }));
return ( <SortableList items={steps} onDragEnd={async (event) => { if (event.canceled || !event.operation.source || !isSortable(event.operation.source)) return;
const result = await q.plugin.timeline.step.reorder.netSend({ pluginId, timelineId, timelineType, stepId: event.operation.source.id as string, newIndex: event.operation.source.index, }); if (result) { setStep(result.stepId); } }} selectedItem={stepId} className="mb-2" /> );}
export function PrerequisiteBlocks({ pluginId, timelineId, timelineType, prerequisiteBlocks,}: { pluginId: string; timelineId: string; timelineType: "reports" | "missions" | "trainings"; prerequisiteBlocks: TimelineBlock[];}) { const variables = timelineType === "reports" ? reportVariableNames : timelineType === "trainings" ? trainingVariableNames : [];
return ( <div className="col-span-2 flex flex-col"> <h3 className="flex items-center text-lg font-medium"> Prerequisites{" "} <InfoTip> <p> These blocks will be executed immediately, including any checks, to evaluate if the timeline is available to be used. Leave blank to always include this timeline. </p> <p>The following variables are available:</p> <ul className="ml-4 list-disc"> {variables.map((a) => ( <li key={a}>{a}</li> ))} </ul> </InfoTip> </h3>{" "} <DefinedVariableProvider variables={[...variables]}> <div className="flex-1 overflow-x-hidden overflow-y-auto"> {prerequisiteBlocks.length === 0 ? ( <div> <p>No prerequisite blocks.</p> <AddBlockButton timelineType={timelineType} executionType={["prerequisite"]} onAddBlock={async (type, init, initOverrides) => { if ( type === "Action" && init && "action" in init && typeof init.action === "string" ) { // @ts-expect-error init.values = getActionPresetValues(init.action, init.values, initOverrides); } await q.plugin.timeline.prerequisiteBlock.add.netSend({ pluginId, timelineId, timelineType, blockType: type, init, }); }} > <RAButton className="btn btn-sm btn-outline btn-success">Add Block</RAButton> </AddBlockButton> </div> ) : ( <SortableBlocks timelineType={timelineType} executionType={["prerequisite"]} blocks={prerequisiteBlocks} onDragEnd={(event) => { if ( event.canceled || !event.operation.source || !isSortable(event.operation.source) ) return; void q.plugin.timeline.prerequisiteBlock.reorder.netSend({ pluginId, timelineId, timelineType, blockId: event.operation.source.id as string, newIndex: event.operation.source.index, }); }} onUpdate={async (block, property, value) => { await q.plugin.timeline.prerequisiteBlock.update.netSend({ pluginId, timelineId, timelineType, blockId: block.id, properties: { [property]: value }, }); }} onReplace={(id, blocks) => { q.plugin.timeline.prerequisiteBlock.replace.netSend({ pluginId, timelineId, timelineType, blockId: id, blocks, }); }} onRemove={(id) => q.plugin.timeline.prerequisiteBlock.delete.netSend({ pluginId, timelineId, timelineType, blockId: id, }) } /> )} </div> <AddBlockButton timelineType={timelineType} executionType={["prerequisite"]} onAddBlock={async (type, init, initOverrides) => { if (type === "Action" && init && "action" in init && typeof init.action === "string") { // @ts-expect-error init.values = getActionPresetValues(init.action, init.values, initOverrides); } await q.plugin.timeline.prerequisiteBlock.add.netSend({ pluginId, timelineId, timelineType, blockType: type, init, }); }} > <RAButton className="btn btn-sm btn-outline btn-success">Add Block</RAButton> </AddBlockButton> </DefinedVariableProvider> </div> );}
export function useGetActionPresetValue(timelineType: "reports" | "missions" | "trainings") { const [actions] = q.thorium.actions.useNetRequest(); const variables = timelineType === "reports" ? reportVariableNames : timelineType === "trainings" ? trainingVariableNames : []; const localVariables = [...variables, ...useDefinedVariables()];
return function getActionPresetValues(actionName: string, initValues = {}, initOverrides = true) { const action = actions.find((a) => a.action === actionName); const actionSchema = action ? // oxlint-disable-next-line no-eval parseSchema(eval(parseJsonSchema(action.input)), {}) : []; let values = initValues; const actionInputs = actionSchema.map((a) => a.key); for (const actionInput of actionInputs) { if (localVariables.includes(actionInput)) { if (initOverrides) { values = { [actionInput]: `$${actionInput}`, ...values }; } else { values = { ...values, [actionInput]: `$${actionInput}` }; } } } return values; };}