f270812219
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
|
|
import { isNpcBindingNone, listStorylineOptionsForBinding, noneBinding } from '../../shared/npcs/npcBinding';
|
|
import type { GraphNodeId, NpcBinding, Project, SceneId } from '../../shared/types';
|
|
import editorStyles from '../editor/EditorApp.module.css';
|
|
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
|
import { Select } from '../shared/ui/controls';
|
|
|
|
type NpcBindingFieldsProps = {
|
|
project: Project;
|
|
binding: NpcBinding;
|
|
onChange: (binding: NpcBinding) => void;
|
|
};
|
|
|
|
function defaultBinding(project: Project): NpcBinding {
|
|
const opts = listStorylineOptionsForBinding(project);
|
|
if (opts.main) return { kind: 'storyline', storyline: { kind: 'main' } };
|
|
if (opts.sides[0]) {
|
|
return {
|
|
kind: 'storyline',
|
|
storyline: { kind: 'side', startGraphNodeId: opts.sides[0].startGraphNodeId },
|
|
};
|
|
}
|
|
const firstScene = Object.keys(project.scenes)[0] as SceneId | undefined;
|
|
if (firstScene) return { kind: 'scene', sceneId: firstScene };
|
|
return noneBinding();
|
|
}
|
|
|
|
export function NpcBindingFields({ project, binding, onChange }: NpcBindingFieldsProps) {
|
|
const { t } = useEditorI18n();
|
|
const enabled = !isNpcBindingNone(binding);
|
|
const storylineOpts = useMemo(() => listStorylineOptionsForBinding(project), [project]);
|
|
const sceneOptions = useMemo(
|
|
() =>
|
|
Object.entries(project.scenes)
|
|
.map(([id, scene]) => ({ id: id as SceneId, title: scene.title.trim() || id }))
|
|
.sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })),
|
|
[project.scenes],
|
|
);
|
|
|
|
const kind = binding.kind === 'none' ? 'storyline' : binding.kind;
|
|
|
|
const bindingTargetValue = useMemo(() => {
|
|
if (binding.kind === 'scene') return binding.sceneId;
|
|
if (binding.kind === 'storyline') {
|
|
if (binding.storyline.kind === 'main') return 'main';
|
|
return `side:${binding.storyline.startGraphNodeId}`;
|
|
}
|
|
return '';
|
|
}, [binding]);
|
|
|
|
return (
|
|
<div className={editorStyles.fieldGrid}>
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={enabled}
|
|
onChange={(e) => {
|
|
onChange(e.target.checked ? defaultBinding(project) : noneBinding());
|
|
}}
|
|
/>
|
|
<span>{t('npcs.bindingEnable')}</span>
|
|
</label>
|
|
|
|
{enabled ? (
|
|
<>
|
|
<div className={editorStyles.fieldLabel}>{t('npcs.bindingKind')}</div>
|
|
<Select
|
|
value={kind}
|
|
ariaLabel={t('npcs.bindingKind')}
|
|
options={[
|
|
{ value: 'storyline', label: t('npcs.bindingStoryline') },
|
|
{ value: 'scene', label: t('npcs.bindingScene') },
|
|
]}
|
|
onChange={(nextKind) => {
|
|
if (nextKind === 'scene') {
|
|
const first = sceneOptions[0];
|
|
onChange(first ? { kind: 'scene', sceneId: first.id } : noneBinding());
|
|
return;
|
|
}
|
|
if (storylineOpts.main) {
|
|
onChange({ kind: 'storyline', storyline: { kind: 'main' } });
|
|
} else if (storylineOpts.sides[0]) {
|
|
onChange({
|
|
kind: 'storyline',
|
|
storyline: { kind: 'side', startGraphNodeId: storylineOpts.sides[0].startGraphNodeId },
|
|
});
|
|
} else {
|
|
onChange(noneBinding());
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<div className={editorStyles.fieldLabel}>{t('npcs.bindingSelect')}</div>
|
|
<Select
|
|
value={bindingTargetValue}
|
|
ariaLabel={t('npcs.bindingSelect')}
|
|
options={
|
|
kind === 'storyline'
|
|
? [
|
|
...(storylineOpts.main
|
|
? [{ value: 'main', label: t('npcs.bindingMain') }]
|
|
: []),
|
|
...storylineOpts.sides.map((s) => ({
|
|
value: `side:${s.startGraphNodeId}`,
|
|
label: s.label,
|
|
})),
|
|
]
|
|
: sceneOptions.map((s) => ({ value: s.id, label: s.title }))
|
|
}
|
|
onChange={(v) => {
|
|
if (kind === 'scene') {
|
|
onChange({ kind: 'scene', sceneId: v as SceneId });
|
|
return;
|
|
}
|
|
if (v === 'main') {
|
|
onChange({ kind: 'storyline', storyline: { kind: 'main' } });
|
|
return;
|
|
}
|
|
if (v.startsWith('side:')) {
|
|
onChange({
|
|
kind: 'storyline',
|
|
storyline: {
|
|
kind: 'side',
|
|
startGraphNodeId: v.slice(5) as GraphNodeId,
|
|
},
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|