import type { ComponentQuery, EntityQuery, ValueQuery, } from "@thorium/.server/classes/Plugins/TimelineStep"; import { components, type ComponentIds } from "@thorium/ecs-components"; import { SoundConfigForm } from "@thorium/routes/config/systems/soundId"; import Checkbox from "@thorium/ui/Checkbox"; import { popoverTransitionClasses } from "@thorium/ui/Dropdown"; import { Icon } from "@thorium/ui/Icon"; import Input from "@thorium/ui/Input"; import Select from "@thorium/ui/Select"; import TagInput from "@thorium/ui/TagInput"; import { Tooltip } from "@thorium/ui/Tooltip"; import { cn } from "@thorium/utils/cn"; import { type InputTypes, ZOD_COMPARISONS, getInputType, parseSchema, schemaWithoutDefault, } from "@thorium/utils/zodAutoForm"; import { capitalCase } from "change-case"; import { produce } from "immer"; import { type Dispatch, useId } from "react"; import { Button, ComboBox, ListBox, ListBoxItem, Popover, Input as RAInput, } from "react-aria-components"; import { ShipTemplate } from "./ShipTemplate"; import { StarmapCoordinates } from "./StarmapCoordinates"; type QueryReducerAction = | { type: "add"; component?: keyof typeof components | ""; path?: string } | { type: "remove"; path: string } | { type: "component"; path: string; value: keyof typeof components | "" } | { type: "property"; path: string; value: string; comparison: string | null; } | { type: "comparison"; path: string; value: string | null } | { type: "value"; path: string; value: string | ValueQuery } | { type: "matchType"; path: string; value: "all" | "first" | "random" }; export function queryReducer(state: EntityQuery, action: QueryReducerAction): EntityQuery { switch (action.type) { case "add": return produce(state, (draft) => { getObject(draft, action.path || "").push({ component: action.component || "", property: "", comparison: null, value: "", }); }); case "remove": return produce(state, (draft) => { const path = action.path.split(".").slice(0, -1).join("."); let index: number | string | undefined = action.path.split(".").pop(); index = Number.isNaN(Number(index)) ? index : Number(index); getObject(draft, path).splice(index, 1); }); case "component": return produce(state, (draft) => { getObject(draft, action.path).component = action.value; getObject(draft, action.path).property = "isPresent"; getObject(draft, action.path).comparison = null; }); case "property": return produce(state, (draft) => { getObject(draft, action.path).property = action.value; getObject(draft, action.path).comparison = action.comparison; }); case "comparison": return produce(state, (draft) => { getObject(draft, action.path).comparison = action.value; }); case "value": return produce(state, (draft) => { getObject(draft, action.path).value = action.value; }); case "matchType": return produce(state, (draft) => { getObject(draft, action.path).matchType = action.value; }); default: return state; } } export function getObject(object: any, path: string | null) { if (!path) return object; const paths = path.split(".").map((p) => (Number.isNaN(Number(p)) ? p : Number(p))); const target = paths.reduce((obj, key) => { if (!obj[key]) obj[key] = typeof key === "number" ? [] : {}; return obj[key]; }, object) || object; return target; } const matchItems = [ { id: "all", label: "All Matches" }, { id: "first", label: "First Match" }, { id: "random", label: "Random Match" }, ]; function QueryComponent({ component, property, value, comparison, path, dispatch, showDelete, }: Omit & Partial> & { path: string; dispatch: Dispatch; showDelete: boolean; }) { const item = component ? parseSchema(schemaWithoutDefault(component as ComponentIds)).find((p) => p.key === property) : null; const isSelect = path.endsWith(".select"); return (
{ dispatch({ type: "component", path, value }); }} isSelect={isSelect} /> { const item = component ? parseSchema(schemaWithoutDefault(component as ComponentIds)).find( (p) => p.key === value, ) : null; const comparison = ZOD_COMPARISONS[item?.type as keyof typeof ZOD_COMPARISONS]?.[0] || null; dispatch({ type: "property", path, value, comparison }); }} onlyShowProperties={isSelect} /> {!isSelect && typeof comparison === "string" && property && !["isPresent", "isNotPresent"].includes(property) ? ( { dispatch({ type: "comparison", path, value }); }} /> ) : null} {!isSelect && property && comparison && item?.type !== "ZodBoolean" && item ? ( <> {item.helper &&

{item.helper}

} {showDelete ? dispatch({ type: "remove", path })} /> : null} ) : null} {showDelete && (!comparison || !property) ? ( dispatch({ type: "remove", path })} /> ) : null}
); } const voices = typeof window === "undefined" ? [] : window.speechSynthesis .getVoices() .filter((s) => s.lang === navigator.language) .map((s) => s.name); export function ValueInput({ value, item, dispatch, path, queryInput, }: { value: string | ValueQuery | undefined; item: { key: string; name: string; type: any; values: any; helper?: string; inputProps: React.InputHTMLAttributes; isNested: boolean; }; dispatch: React.Dispatch; path: string; queryInput?: boolean; }) { const id = useId(); const noEntityQuery = item.type === "components"; // Special override for the voice input if (item.key.endsWith("voice")) { return (
dispatch({ type: "value", path: queryInput ? path : `${path}.values.${item.key}`, value, }) } label={item.name} labelHidden {...item.inputProps} />
); } return !(typeof value === "object" && "query" in value && "select" in value) ? (
{queryInput ? null : }
{noEntityQuery ? null : ( )}
) : typeof value === "object" ? (

Entity Query

{value.query.map((q, i) => ( 1} /> ))}

ID/Component Select

{ if (Array.isArray(value)) return; setComparison(value ? value : null); }} /> ); } function ComponentCombobox({ component, onChange, isSelect, }: { component: keyof typeof components | ""; onChange: (value: keyof typeof components | "") => void; isSelect?: boolean; }) { return ( onChange(selection as keyof typeof components)} >
({ id })), ].flat()} > {(item) => ( {capitalCase(item.id)} )}
); } export function PropertyCombobox({ component, property, onChange, onlyShowProperties, }: { component: keyof typeof components | "id" | ""; property: string; onChange: (value: string) => void; onlyShowProperties?: boolean; }) { if (component === "id") return null; const properties = !component ? [] : [ ...(onlyShowProperties ? [] : ["isPresent", "isNotPresent"]), ...parseSchema(schemaWithoutDefault(component)).map((item) => ZOD_COMPARISONS[item.type as keyof typeof ZOD_COMPARISONS] ? item.key : [], ), ] .flat() .map((id) => ({ id })); return ( onChange(selection as string)} >
{(item) => ( {capitalCase(item.id)} )}
); } export function PropertyInput({ inputType, inputValues, value, setValue, label, labelHidden = true, multiple, id, ...inputProps }: { inputType: InputTypes; inputValues?: string[]; value?: any; setValue: (value: any) => void; label: string; labelHidden?: boolean; multiple?: boolean; id: string; }) { switch (inputType) { case "number": return ( setValue(e.target.value)} defaultValue={value} {...inputProps} /> ); case "checkbox": return ( setValue(e.target.checked)} checked={value} {...inputProps} /> ); case "select": return ( setValue(e.target.value)} value={value} {...inputProps} /> ); case "tags": return ( setValue([...(value || []), t].filter((a, i, arr) => arr.indexOf(a) === i))} onRemove={(t) => setValue(value?.filter((v: any) => v !== t) || [])} {...inputProps} /> ); case "object": return null; case "starmapCoordinates": return ; case "shipTemplate": return ; case "components": return ; case "sound": { const sound = value || { url: "", volume: [1, 1], playbackRate: [1, 1], loop: false, loopStart: null, loopEnd: null, delay: 0, loopGap: 0, channel: null, }; return ( setValue({ ...sound, [property]: value })} /> ); } case "textarea": return ( setValue(e.target.value)} defaultValue={value} {...inputProps} /> ); default: if (inputType !== "text") { console.warn("Unknown input type", inputType); } return ( setValue(e.target.value)} defaultValue={value} {...inputProps} /> ); } } function ComponentsEditor({ components, setValue, }: { components: EntityQuery; setValue: (value: EntityQuery) => void; }) { return (
{!components || components.length === 0 ? ( setValue(queryReducer(components || [], { type: "add", component })) } /> ) : ( components?.map(({ component, property, value }, i) => { const item = component ? parseSchema(schemaWithoutDefault(component as ComponentIds)).find( (p) => p.key === property, ) : null; return (
setValue( queryReducer(components, { type: "component", path: i.toString(), value: component, }), ) } /> setValue( queryReducer(components, { type: "property", path: i.toString(), comparison: "", value: property, }), ) } /> {item ? ( <> setValue(queryReducer(components, action))} queryInput /> {item.helper &&

{item.helper}

} {/* {showDelete ? ( dispatch({ type: "remove", path })} /> ) : null} */} ) : null}
); }) )}
); }