2024-06-02 20:07:35 +00:00
|
|
|
import { Maybe, doesExist, mustDefault, mustExist } from '@apextoaster/js-utils';
|
2024-06-02 21:42:47 +00:00
|
|
|
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControlLabel, MenuItem, Stack, Switch, TextField } from '@mui/material';
|
2024-06-02 20:07:35 +00:00
|
|
|
import React from 'react';
|
|
|
|
import { useStore } from 'zustand';
|
2024-06-02 21:42:47 +00:00
|
|
|
import { World, BooleanParameter, NumberParameter, StringParameter, Parameter, Action } from './models';
|
2024-06-02 20:07:35 +00:00
|
|
|
import { StoreState, store } from './store';
|
|
|
|
|
|
|
|
// region parameter components
|
|
|
|
export interface BooleanParameterProps {
|
|
|
|
name: string;
|
2024-06-02 21:42:47 +00:00
|
|
|
parameter: BooleanParameter;
|
2024-06-02 20:07:35 +00:00
|
|
|
|
|
|
|
setParameter: (value: boolean) => void;
|
|
|
|
}
|
|
|
|
|
2024-06-02 21:42:47 +00:00
|
|
|
export function BooleanParameterItem(props: BooleanParameterProps) {
|
|
|
|
const { name, setParameter } = props;
|
|
|
|
|
|
|
|
return <FormControlLabel control={<Switch onChange={(event) => setParameter(event.target.checked)} />} label={name} />;
|
2024-06-02 20:07:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function EnumParameterItem(props: NumberParameterProps | StringParameterProps) {
|
|
|
|
const { name, parameter, setParameter } = props;
|
|
|
|
const enumValues = mustExist(parameter.enum);
|
|
|
|
const defaultValue = mustDefault(parameter.default, enumValues[0]);
|
|
|
|
|
|
|
|
return <TextField
|
|
|
|
select
|
|
|
|
label={name}
|
|
|
|
variant="outlined"
|
|
|
|
defaultValue={defaultValue}
|
|
|
|
onChange={(event) => setParameter(event.target.value)}
|
|
|
|
>
|
|
|
|
{enumValues.map((value) => <MenuItem value={value}>{value}</MenuItem>)}
|
|
|
|
</TextField>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface NumberParameterProps {
|
|
|
|
name: string;
|
|
|
|
parameter: NumberParameter;
|
|
|
|
|
|
|
|
setParameter: (value: number) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function NumberParameterItem(props: NumberParameterProps) {
|
|
|
|
const { name, parameter, setParameter } = props;
|
|
|
|
|
|
|
|
if (doesExist(parameter.enum)) {
|
|
|
|
return <EnumParameterItem name={name} parameter={parameter} setParameter={setParameter} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <TextField
|
|
|
|
label={name}
|
|
|
|
variant="outlined"
|
|
|
|
defaultValue={parameter.default}
|
|
|
|
onChange={(event) => setParameter(parseFloat(event.target.value))}
|
|
|
|
/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface StringParameterProps {
|
|
|
|
name: string;
|
|
|
|
parameter: StringParameter;
|
|
|
|
|
|
|
|
setParameter: (value: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function StringParameterItem(props: StringParameterProps) {
|
|
|
|
const { name, parameter, setParameter } = props;
|
|
|
|
|
|
|
|
if (doesExist(parameter.enum)) {
|
|
|
|
return <EnumParameterItem name={name} parameter={parameter} setParameter={setParameter} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <TextField
|
|
|
|
label={name}
|
|
|
|
variant="outlined"
|
|
|
|
defaultValue={parameter.default}
|
|
|
|
onChange={(event) => setParameter(event.target.value)}
|
|
|
|
/>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface UnknownParameterProps {
|
|
|
|
name: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function UnknownParameter(props: UnknownParameterProps) {
|
|
|
|
const { name } = props;
|
|
|
|
|
|
|
|
return <Alert severity="warning">Unknown parameter type: {name}</Alert>;
|
|
|
|
}
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
export const SIGNIFICANT_PARAMETERS = ['character', 'direction', 'item', 'room', 'target'];
|
|
|
|
|
|
|
|
export function listCharacters(world: World) {
|
|
|
|
return world.rooms.flatMap((room) => room.characters);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function listItems(world: World) {
|
|
|
|
return world.rooms.flatMap((room) => room.items);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function listPortals(world: World) {
|
|
|
|
return world.rooms.flatMap((room) => room.portals);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function enumerateSignificantParameterValues(name: string, world: World) {
|
|
|
|
switch (name) {
|
|
|
|
case 'character':
|
|
|
|
return listCharacters(world).map((character) => character.name);
|
|
|
|
case 'direction':
|
|
|
|
return listPortals(world).map((portal) => portal.name);
|
|
|
|
case 'item':
|
|
|
|
return listItems(world).map((item) => item.name);
|
|
|
|
case 'room':
|
|
|
|
return world.rooms.map((room) => room.name);
|
|
|
|
case 'target':
|
|
|
|
{
|
|
|
|
const characters = listCharacters(world);
|
|
|
|
const items = listItems(world);
|
|
|
|
|
|
|
|
return [
|
|
|
|
...characters.map((character) => character.name),
|
|
|
|
...items.map((item) => item.name),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-03 01:00:17 +00:00
|
|
|
export function convertSignificantParameter<T extends Parameter>(name: string, parameter: T, world: Maybe<World>): T {
|
2024-06-02 21:42:47 +00:00
|
|
|
if (parameter.type === 'boolean') {
|
|
|
|
return parameter;
|
|
|
|
}
|
|
|
|
|
2024-06-02 20:07:35 +00:00
|
|
|
if (doesExist(world) && SIGNIFICANT_PARAMETERS.includes(name)) {
|
|
|
|
return {
|
|
|
|
...parameter,
|
|
|
|
enum: enumerateSignificantParameterValues(name, world),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return parameter;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function selectWorld(state: StoreState) {
|
|
|
|
return {
|
|
|
|
world: state.world,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-06-02 21:42:47 +00:00
|
|
|
export function formatAction(action: string, parameters: Record<string, boolean | number | string>) {
|
2024-06-02 20:07:35 +00:00
|
|
|
return `~${action}:${Object.entries(parameters).map(([name, value]) => `${name}=${value}`).join(',')}`;
|
|
|
|
}
|
|
|
|
|
2024-06-03 01:00:17 +00:00
|
|
|
export function getEnumOrDefault<T>(defaultValue: Maybe<T>, enumValues: Maybe<Array<T>>, evenMoreDefault: T): T {
|
|
|
|
if (doesExist(defaultValue)) {
|
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (doesExist(enumValues)) {
|
|
|
|
return enumValues[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
return evenMoreDefault;
|
|
|
|
}
|
|
|
|
|
2024-06-02 21:42:47 +00:00
|
|
|
export function makeDefaultParameterValues(parameters: Record<string, Parameter>) {
|
|
|
|
return Object.entries(parameters).reduce((acc, [name, parameter]) => {
|
|
|
|
switch (parameter.type) {
|
|
|
|
case 'boolean':
|
2024-06-03 01:00:17 +00:00
|
|
|
return { ...acc, [name]: getEnumOrDefault(parameter.default, [], false) };
|
2024-06-02 21:42:47 +00:00
|
|
|
case 'number':
|
2024-06-03 01:00:17 +00:00
|
|
|
return { ...acc, [name]: getEnumOrDefault(parameter.default, parameter.enum, 0) };
|
2024-06-02 21:42:47 +00:00
|
|
|
case 'string':
|
2024-06-03 01:00:17 +00:00
|
|
|
return { ...acc, [name]: getEnumOrDefault(parameter.default, parameter.enum, '') };
|
2024-06-02 21:42:47 +00:00
|
|
|
default:
|
|
|
|
return acc;
|
|
|
|
}
|
|
|
|
}, {} as Record<string, boolean | number | string>);
|
|
|
|
}
|
|
|
|
|
2024-06-02 20:07:35 +00:00
|
|
|
export interface PromptActionProps {
|
|
|
|
action: Action;
|
|
|
|
|
|
|
|
setAction: (action: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function PromptAction(props: PromptActionProps) {
|
|
|
|
const { action, setAction } = props;
|
|
|
|
const { world } = useStore(store, selectWorld);
|
|
|
|
|
2024-06-02 21:42:47 +00:00
|
|
|
// initialize with default values
|
|
|
|
const [parameterValues, setParameterValues] = React.useState(makeDefaultParameterValues(action.function.parameters.properties));
|
2024-06-02 20:07:35 +00:00
|
|
|
|
|
|
|
// create an input for each parameter
|
2024-06-02 21:42:47 +00:00
|
|
|
const inputs = Object.entries(action.function.parameters.properties).filter(([name, _parameter]) => name !== 'unused').map(([name, parameter]) => {
|
2024-06-02 20:07:35 +00:00
|
|
|
const convertedParameter = convertSignificantParameter(name, parameter, world);
|
|
|
|
|
|
|
|
switch (convertedParameter.type) {
|
|
|
|
case 'string':
|
|
|
|
return <StringParameterItem name={name} parameter={convertedParameter as StringParameter} setParameter={(value) => {
|
|
|
|
setParameterValues((old) => ({ ...old, [name]: value }));
|
|
|
|
}} />;
|
|
|
|
case 'number':
|
|
|
|
return <NumberParameterItem name={name} parameter={convertedParameter as NumberParameter} setParameter={(value) => {
|
|
|
|
setParameterValues((old) => ({ ...old, [name]: value }));
|
|
|
|
}} />;
|
2024-06-02 21:42:47 +00:00
|
|
|
case 'boolean':
|
|
|
|
return <BooleanParameterItem name={name} parameter={convertedParameter as BooleanParameter} setParameter={(value) => {
|
|
|
|
setParameterValues((old) => ({ ...old, [name]: value }));
|
|
|
|
}} />;
|
2024-06-02 20:07:35 +00:00
|
|
|
default:
|
|
|
|
return <UnknownParameter name={name} />;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return <Stack direction='column' spacing={2}>
|
|
|
|
<Button onClick={() => setAction(formatAction(action.function.name, parameterValues))}>{action.function.description}</Button>
|
|
|
|
<Stack direction='row' spacing={2}>
|
|
|
|
{inputs}
|
|
|
|
</Stack>
|
|
|
|
</Stack>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function selectPromptEvent(state: StoreState) {
|
|
|
|
return {
|
|
|
|
promptEvent: state.promptEvent,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface PromptDialogProps {
|
|
|
|
sendInput: (input: string) => void;
|
|
|
|
skipPrompt: () => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function PromptDialog(props: PromptDialogProps) {
|
|
|
|
const { sendInput, skipPrompt } = props;
|
|
|
|
const { promptEvent } = useStore(store, selectPromptEvent);
|
|
|
|
|
|
|
|
const [input, setInput] = React.useState<string>('');
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
|
|
if (!doesExist(promptEvent)) {
|
|
|
|
return <Dialog open={false} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <Dialog open={true}>
|
|
|
|
<DialogTitle>It's your turn, {promptEvent.character.name}!</DialogTitle>
|
|
|
|
<DialogContent>
|
|
|
|
<Stack direction="column" spacing={2}>
|
|
|
|
<DialogContentText>{promptEvent.prompt}</DialogContentText>
|
|
|
|
<Divider />
|
|
|
|
<DialogContentText>Select parameters first, then click the label to act.</DialogContentText>
|
|
|
|
{promptEvent.actions.map((action: Action) => <PromptAction action={action} setAction={setInput} />)}
|
|
|
|
<Divider />
|
|
|
|
<Stack direction="row" spacing={2}>
|
|
|
|
<TextField
|
|
|
|
fullWidth
|
|
|
|
label="Input"
|
|
|
|
variant="outlined"
|
|
|
|
value={input}
|
|
|
|
onChange={(event) => setInput(event.target.value)}
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
sendInput(input);
|
|
|
|
setInput('');
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<Button variant="contained" onClick={() => {
|
|
|
|
sendInput(input);
|
|
|
|
setInput('');
|
|
|
|
}}>Enqueue</Button>
|
|
|
|
</Stack>
|
|
|
|
</Stack>
|
|
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
|
|
<Button onClick={skipPrompt}>Skip</Button>
|
|
|
|
<Button onClick={skipPrompt}>Leave</Button>
|
|
|
|
</DialogActions>
|
|
|
|
</Dialog>;
|
|
|
|
}
|