feat(npcs): add campaign NPCs with relation graph and session overlay
Add a dedicated NPC editor window, directed relations, control/presentation avatar overlay, and ru/en help for the new section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,9 +17,10 @@ import { PixiEffectsOverlay } from '../shared/effects/PxiEffectsOverlay';
|
||||
import { SceneDarknessOverlay } from '../shared/effects/SceneDarknessOverlay';
|
||||
import { useEffectsState } from '../shared/effects/useEffectsState';
|
||||
import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState';
|
||||
import { DEFAULT_MATERIALS_OVERLAY_LAYOUT } from '../../shared/types';
|
||||
import { DEFAULT_MATERIALS_OVERLAY_LAYOUT, DEFAULT_NPCS_OVERLAY_LAYOUT } from '../../shared/types';
|
||||
import { MaterialOverlay } from '../shared/materials/MaterialOverlay';
|
||||
import { useMaterialsOverlayState } from '../shared/materials/useMaterialsOverlayState';
|
||||
import { useNpcsOverlayState } from '../shared/npcs/useNpcsOverlayState';
|
||||
import { Button } from '../shared/ui/controls';
|
||||
import { Surface } from '../shared/ui/Surface';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
@@ -91,6 +92,7 @@ export function ControlApp() {
|
||||
const [fxState, fx] = useEffectsState();
|
||||
const [sdState, sd] = useSceneDarknessState();
|
||||
const [materialsOverlay, materialsApi] = useMaterialsOverlayState();
|
||||
const [npcsOverlay, npcsApi] = useNpcsOverlayState();
|
||||
const [session, setSession] = useState<SessionState | null>(null);
|
||||
const historyRef = useRef<GraphNodeId[]>([]);
|
||||
const [history, setHistory] = useState<GraphNodeId[]>([]);
|
||||
@@ -1203,6 +1205,28 @@ export function ControlApp() {
|
||||
</svg>
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
title={t('control.npcsTool')}
|
||||
ariaLabel={t('control.npcsTool')}
|
||||
onClick={() => {
|
||||
void api.invoke(ipcChannels.windows.openNpcs, {}).catch((err) => {
|
||||
console.error('[control] openNpcs failed', err);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className={styles.iconGlyph} aria-hidden>
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
|
||||
<circle cx="12" cy="8" r="3.4" fill="#3b82f6" />
|
||||
<path
|
||||
fill="#22c55e"
|
||||
d="M5.2 19.2c.6-3.4 3.2-5.2 6.8-5.2s6.2 1.8 6.8 5.2c.1.5-.3 1-.8 1H6c-.5 0-.9-.5-.8-1z"
|
||||
/>
|
||||
<circle cx="12" cy="8" r="1.4" fill="#93c5fd" />
|
||||
</svg>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.spacer12} />
|
||||
{!isVideoPreviewScene ? (
|
||||
@@ -1586,6 +1610,32 @@ export function ControlApp() {
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{(() => {
|
||||
const activeNpc =
|
||||
session?.project && npcsOverlay?.activeNpcId
|
||||
? (session.project.npcs ?? []).find((n) => n.id === npcsOverlay.activeNpcId)
|
||||
: undefined;
|
||||
if (!activeNpc) return null;
|
||||
return (
|
||||
<MaterialOverlay
|
||||
assetId={activeNpc.avatarAssetId}
|
||||
layout={npcsOverlay?.layout ?? DEFAULT_NPCS_OVERLAY_LAYOUT}
|
||||
editable
|
||||
zoomTool={npcsOverlay?.zoomTool ?? null}
|
||||
showClose
|
||||
closeLabel={t('npcs.closeOverlay')}
|
||||
onClose={() => {
|
||||
void npcsApi.dispatch({ kind: 'hide' });
|
||||
}}
|
||||
onLayoutChange={(layout) => {
|
||||
void npcsApi.dispatch({ kind: 'layout.set', layout });
|
||||
}}
|
||||
onZoomAt={(nx, ny) => {
|
||||
void npcsApi.dispatch({ kind: 'zoomAt', nx, ny });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</Surface>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user