fix: control UX — no default tool, launch loader, NPC layout
Deselect effect tools by default and on re-click, block the editor during session launch, and fix NPC overlay/window empty-state layout. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,11 @@ import test from 'node:test';
|
|||||||
|
|
||||||
import { EffectsStore } from './effectsStore';
|
import { EffectsStore } from './effectsStore';
|
||||||
|
|
||||||
|
void test('defaultTool: при старте инструмент не выбран', () => {
|
||||||
|
const store = new EffectsStore();
|
||||||
|
assert.equal(store.getState().tool.tool, 'none');
|
||||||
|
});
|
||||||
|
|
||||||
void test('pruneExpired: лёд не удаляется по времени', () => {
|
void test('pruneExpired: лёд не удаляется по времени', () => {
|
||||||
const store = new EffectsStore();
|
const store = new EffectsStore();
|
||||||
const createdAtMs = Date.now() - 365 * 24 * 60 * 60 * 1000;
|
const createdAtMs = Date.now() - 365 * 24 * 60 * 60 * 1000;
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ function nowMs(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultTool(): EffectToolState {
|
function defaultTool(): EffectToolState {
|
||||||
return { tool: 'fog', radiusN: 0.08, intensity: 0.6 };
|
return { tool: 'none', radiusN: 0.08, intensity: 0.6 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сброс выбранного инструмента (при старте сессии). */
|
||||||
|
export function effectsDefaultTool(): EffectToolState {
|
||||||
|
return defaultTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EffectsStore {
|
export class EffectsStore {
|
||||||
|
|||||||
+3
-1
@@ -19,7 +19,7 @@ import {
|
|||||||
} from '../shared/project/projectZipExtension';
|
} from '../shared/project/projectZipExtension';
|
||||||
import type { Project } from '../shared/types';
|
import type { Project } from '../shared/types';
|
||||||
|
|
||||||
import { EffectsStore } from './effects/effectsStore';
|
import { EffectsStore, effectsDefaultTool } from './effects/effectsStore';
|
||||||
import { SceneDarknessStore } from './effects/sceneDarknessStore';
|
import { SceneDarknessStore } from './effects/sceneDarknessStore';
|
||||||
import { SceneTrapsStore } from './sceneTraps/sceneTrapsStore';
|
import { SceneTrapsStore } from './sceneTraps/sceneTrapsStore';
|
||||||
import { installIpcRouter, registerHandler, setLicenseAssert } from './ipc/router';
|
import { installIpcRouter, registerHandler, setLicenseAssert } from './ipc/router';
|
||||||
@@ -378,6 +378,7 @@ async function main() {
|
|||||||
registerHandler(ipcChannels.windows.openMultiWindow, () => {
|
registerHandler(ipcChannels.windows.openMultiWindow, () => {
|
||||||
sceneDarknessStore.resetSession();
|
sceneDarknessStore.resetSession();
|
||||||
sceneTrapsStore.resetSession();
|
sceneTrapsStore.resetSession();
|
||||||
|
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
|
||||||
openMultiWindow();
|
openMultiWindow();
|
||||||
const project = projectStore.getOpenProject();
|
const project = projectStore.getOpenProject();
|
||||||
if (project) {
|
if (project) {
|
||||||
@@ -386,6 +387,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
emitSceneDarknessState();
|
emitSceneDarknessState();
|
||||||
emitSceneTrapsState();
|
emitSceneTrapsState();
|
||||||
|
emitEffectsState();
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
});
|
});
|
||||||
registerHandler(ipcChannels.windows.closeMultiWindow, () => {
|
registerHandler(ipcChannels.windows.closeMultiWindow, () => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
PixiEffectsOverlay,
|
PixiEffectsOverlay,
|
||||||
type PixiEffectsOverlayHandle,
|
type PixiEffectsOverlayHandle,
|
||||||
} from '../shared/effects/PxiEffectsOverlay';
|
} from '../shared/effects/PxiEffectsOverlay';
|
||||||
import type { EffectInstance, ExplosionInstance } from '../../shared/types/effects';
|
import type { EffectInstance, EffectToolType, ExplosionInstance } from '../../shared/types/effects';
|
||||||
import { SceneDarknessOverlay } from '../shared/effects/SceneDarknessOverlay';
|
import { SceneDarknessOverlay } from '../shared/effects/SceneDarknessOverlay';
|
||||||
import { useEffectsState } from '../shared/effects/useEffectsState';
|
import { useEffectsState } from '../shared/effects/useEffectsState';
|
||||||
import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState';
|
import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState';
|
||||||
@@ -839,10 +839,16 @@ export function ControlApp() {
|
|||||||
const showReturnToMain = isInSideStoryline && mainStoryReturnGraphNodeId !== null;
|
const showReturnToMain = isInSideStoryline && mainStoryReturnGraphNodeId !== null;
|
||||||
const branchOptionOffset = showReturnToMain ? 1 : 0;
|
const branchOptionOffset = showReturnToMain ? 1 : 0;
|
||||||
|
|
||||||
const tool = fxState?.tool ?? { tool: 'fog', radiusN: 0.08, intensity: 0.6 };
|
const tool = fxState?.tool ?? { tool: 'none' as const, radiusN: 0.08, intensity: 0.6 };
|
||||||
const toolRef = useRef(tool);
|
const toolRef = useRef(tool);
|
||||||
toolRef.current = tool;
|
toolRef.current = tool;
|
||||||
|
|
||||||
|
/** Повторный клик по активному инструменту снимает выбор. */
|
||||||
|
function selectEffectTool(next: Exclude<EffectToolType, 'none'>): void {
|
||||||
|
const toolName: EffectToolType = tool.tool === next ? 'none' : next;
|
||||||
|
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: toolName } });
|
||||||
|
}
|
||||||
|
|
||||||
function layoutBrushCursor(): void {
|
function layoutBrushCursor(): void {
|
||||||
const el = brushCursorElRef.current;
|
const el = brushCursorElRef.current;
|
||||||
const p = cursorPosRef.current;
|
const p = cursorPosRef.current;
|
||||||
@@ -850,7 +856,7 @@ export function ControlApp() {
|
|||||||
const ps = previewSizeRef.current;
|
const ps = previewSizeRef.current;
|
||||||
const t = toolRef.current;
|
const t = toolRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (!p) {
|
if (!p || t.tool === 'none') {
|
||||||
el.style.visibility = 'hidden';
|
el.style.visibility = 'hidden';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1447,7 +1453,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.eraser')}
|
title={t('control.eraser')}
|
||||||
ariaLabel={t('control.eraser')}
|
ariaLabel={t('control.eraser')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'eraser' } })}
|
onClick={() => selectEffectTool('eraser')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🧹</span>
|
<span className={styles.iconGlyph}>🧹</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1483,7 +1489,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.fog')}
|
title={t('control.fog')}
|
||||||
ariaLabel={t('control.fog')}
|
ariaLabel={t('control.fog')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'fog' } })}
|
onClick={() => selectEffectTool('fog')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🌫️</span>
|
<span className={styles.iconGlyph}>🌫️</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1492,7 +1498,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.rain')}
|
title={t('control.rain')}
|
||||||
ariaLabel={t('control.rain')}
|
ariaLabel={t('control.rain')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'rain' } })}
|
onClick={() => selectEffectTool('rain')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🌧️</span>
|
<span className={styles.iconGlyph}>🌧️</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1501,7 +1507,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.fire')}
|
title={t('control.fire')}
|
||||||
ariaLabel={t('control.fire')}
|
ariaLabel={t('control.fire')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'fire' } })}
|
onClick={() => selectEffectTool('fire')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🔥</span>
|
<span className={styles.iconGlyph}>🔥</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1510,7 +1516,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.water')}
|
title={t('control.water')}
|
||||||
ariaLabel={t('control.water')}
|
ariaLabel={t('control.water')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'water' } })}
|
onClick={() => selectEffectTool('water')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>💧</span>
|
<span className={styles.iconGlyph}>💧</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1524,9 +1530,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.lightning')}
|
title={t('control.lightning')}
|
||||||
ariaLabel={t('control.lightning')}
|
ariaLabel={t('control.lightning')}
|
||||||
onClick={() =>
|
onClick={() => selectEffectTool('lightning')}
|
||||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'lightning' } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>⚡</span>
|
<span className={styles.iconGlyph}>⚡</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1535,7 +1539,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.sunbeam')}
|
title={t('control.sunbeam')}
|
||||||
ariaLabel={t('control.sunbeam')}
|
ariaLabel={t('control.sunbeam')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'sunbeam' } })}
|
onClick={() => selectEffectTool('sunbeam')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>☀️</span>
|
<span className={styles.iconGlyph}>☀️</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1544,7 +1548,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.freeze')}
|
title={t('control.freeze')}
|
||||||
ariaLabel={t('control.freeze')}
|
ariaLabel={t('control.freeze')}
|
||||||
onClick={() => void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'freeze' } })}
|
onClick={() => selectEffectTool('freeze')}
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>❄️</span>
|
<span className={styles.iconGlyph}>❄️</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1553,9 +1557,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.darkness')}
|
title={t('control.darkness')}
|
||||||
ariaLabel={t('control.darkness')}
|
ariaLabel={t('control.darkness')}
|
||||||
onClick={() =>
|
onClick={() => selectEffectTool('darkness')}
|
||||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🌑</span>
|
<span className={styles.iconGlyph}>🌑</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1564,9 +1566,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.poisonCloud')}
|
title={t('control.poisonCloud')}
|
||||||
ariaLabel={t('control.poisonCloud')}
|
ariaLabel={t('control.poisonCloud')}
|
||||||
onClick={() =>
|
onClick={() => selectEffectTool('poisonCloud')}
|
||||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'poisonCloud' } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>☣️</span>
|
<span className={styles.iconGlyph}>☣️</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1575,9 +1575,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.explosion')}
|
title={t('control.explosion')}
|
||||||
ariaLabel={t('control.explosion')}
|
ariaLabel={t('control.explosion')}
|
||||||
onClick={() =>
|
onClick={() => selectEffectTool('explosion')}
|
||||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'explosion' } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>💥</span>
|
<span className={styles.iconGlyph}>💥</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1592,9 +1590,7 @@ export function ControlApp() {
|
|||||||
iconOnly
|
iconOnly
|
||||||
title={t('control.explorerBrush')}
|
title={t('control.explorerBrush')}
|
||||||
ariaLabel={t('control.explorerBrush')}
|
ariaLabel={t('control.explorerBrush')}
|
||||||
onClick={() =>
|
onClick={() => selectEffectTool('exploreBrush')}
|
||||||
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'exploreBrush' } })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<span className={styles.iconGlyph}>🔦</span>
|
<span className={styles.iconGlyph}>🔦</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1772,6 +1768,7 @@ export function ControlApp() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
if (tool.tool === 'none') return;
|
||||||
const p = toNPoint(e);
|
const p = toNPoint(e);
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
cursorPosRef.current = p;
|
cursorPosRef.current = p;
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ void test('ControlApp: эффекты в пульте, иконки с тулт
|
|||||||
assert.ok(src.includes("title={t('control.poisonCloud')}"));
|
assert.ok(src.includes("title={t('control.poisonCloud')}"));
|
||||||
assert.ok(src.includes("title={t('control.fog')}"));
|
assert.ok(src.includes("title={t('control.fog')}"));
|
||||||
assert.ok(src.includes("ariaLabel={t('control.fog')}"));
|
assert.ok(src.includes("ariaLabel={t('control.fog')}"));
|
||||||
|
assert.ok(src.includes('selectEffectTool'), 'повторный клик снимает инструмент');
|
||||||
|
assert.ok(src.includes("tool.tool === next ? 'none' : next"));
|
||||||
assert.ok(src.includes('iconOnly'));
|
assert.ok(src.includes('iconOnly'));
|
||||||
assert.ok(src.includes("title={t('control.clearEffects')}"));
|
assert.ok(src.includes("title={t('control.clearEffects')}"));
|
||||||
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
|
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export function EditorApp() {
|
|||||||
const [importReport, setImportReport] = useState<StorylineImportMergeReport | null>(null);
|
const [importReport, setImportReport] = useState<StorylineImportMergeReport | null>(null);
|
||||||
const [previewDialogSceneId, setPreviewDialogSceneId] = useState<SceneId | null>(null);
|
const [previewDialogSceneId, setPreviewDialogSceneId] = useState<SceneId | null>(null);
|
||||||
const [presentationOpen, setPresentationOpen] = useState(false);
|
const [presentationOpen, setPresentationOpen] = useState(false);
|
||||||
|
const [launching, setLaunching] = useState(false);
|
||||||
const [licenseSnap, setLicenseSnap] = useState<LicenseSnapshot | null>(null);
|
const [licenseSnap, setLicenseSnap] = useState<LicenseSnapshot | null>(null);
|
||||||
const [checkUpdatesOpen, setCheckUpdatesOpen] = useState(false);
|
const [checkUpdatesOpen, setCheckUpdatesOpen] = useState(false);
|
||||||
const [appPackaged, setAppPackaged] = useState(false);
|
const [appPackaged, setAppPackaged] = useState(false);
|
||||||
@@ -351,15 +352,24 @@ export function EditorApp() {
|
|||||||
|
|
||||||
const launchFromGraphNode = useCallback(
|
const launchFromGraphNode = useCallback(
|
||||||
(graphNodeId: GraphNodeId) => {
|
(graphNodeId: GraphNodeId) => {
|
||||||
if (!licenseActive) return;
|
if (!licenseActive || launching) return;
|
||||||
|
setLaunching(true);
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
try {
|
||||||
await getDndApi().invoke(ipcChannels.project.setCurrentGraphNode, { graphNodeId });
|
await getDndApi().invoke(ipcChannels.project.setCurrentGraphNode, { graphNodeId });
|
||||||
await getDndApi().invoke(ipcChannels.windows.openMultiWindow, {});
|
await getDndApi().invoke(ipcChannels.windows.openMultiWindow, {});
|
||||||
|
} catch {
|
||||||
|
setLaunching(false);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
[licenseActive],
|
[licenseActive, launching],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (presentationOpen) setLaunching(false);
|
||||||
|
}, [presentationOpen]);
|
||||||
|
|
||||||
const currentProjectName = state.project?.meta.name ?? '';
|
const currentProjectName = state.project?.meta.name ?? '';
|
||||||
const currentFileBaseName = state.project?.meta.fileBaseName ?? '';
|
const currentFileBaseName = state.project?.meta.fileBaseName ?? '';
|
||||||
const existingProjectNames = useMemo(() => state.projects.map((p) => p.name), [state.projects]);
|
const existingProjectNames = useMemo(() => state.projects.map((p) => p.name), [state.projects]);
|
||||||
@@ -646,6 +656,17 @@ export function EditorApp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{launching
|
||||||
|
? createPortal(
|
||||||
|
<div className={styles.progressOverlay} role="dialog" aria-label={t('top.launching')} aria-busy>
|
||||||
|
<div className={styles.previewBusyModal}>
|
||||||
|
<div className={styles.previewSpinner} aria-hidden />
|
||||||
|
<div className={styles.previewBusyText}>{t('top.launching')}</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
{presentationOpen
|
{presentationOpen
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<div className={styles.editorLockOverlay} role="dialog" aria-label={t('presentation.overlay')}>
|
<div className={styles.editorLockOverlay} role="dialog" aria-label={t('presentation.overlay')}>
|
||||||
@@ -821,9 +842,9 @@ export function EditorApp() {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={runDisabled}
|
disabled={runDisabled || launching}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!licenseActive || !graphStartGraphNodeId) return;
|
if (!licenseActive || !graphStartGraphNodeId || launching) return;
|
||||||
launchFromGraphNode(graphStartGraphNodeId);
|
launchFromGraphNode(graphStartGraphNodeId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'top.backToProjects': 'К списку проектов',
|
'top.backToProjects': 'К списку проектов',
|
||||||
'top.appVersion': 'Версия приложения',
|
'top.appVersion': 'Версия приложения',
|
||||||
'top.run': 'Запустить',
|
'top.run': 'Запустить',
|
||||||
|
'top.launching': 'Запуск…',
|
||||||
'top.afterLicense': 'Доступно после активации лицензии',
|
'top.afterLicense': 'Доступно после активации лицензии',
|
||||||
'top.setStartScene': 'Назначьте начальную сцену на графе (ПКМ по узлу)',
|
'top.setStartScene': 'Назначьте начальную сцену на графе (ПКМ по узлу)',
|
||||||
'top.runHelpAria': 'Как разблокировать кнопку «Запустить»',
|
'top.runHelpAria': 'Как разблокировать кнопку «Запустить»',
|
||||||
@@ -431,7 +432,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'npcs.graphZoomOut': 'Уменьшить',
|
'npcs.graphZoomOut': 'Уменьшить',
|
||||||
'npcs.graphFitAll': 'Показать всё',
|
'npcs.graphFitAll': 'Показать всё',
|
||||||
'npcs.windowEmpty': 'Добавьте НПС в редакторе.',
|
'npcs.windowEmpty': 'Добавьте НПС в редакторе.',
|
||||||
'npcs.selectToShow': 'Выберите персонажей в списке — они появятся на экране.',
|
'npcs.selectToShow': 'Выберите персонажа в списке — он появится на экране.',
|
||||||
'npcs.closeOverlay': 'Закрыть всех',
|
'npcs.closeOverlay': 'Закрыть всех',
|
||||||
'npcs.rotateOverlay': 'Повернуть',
|
'npcs.rotateOverlay': 'Повернуть',
|
||||||
'npcs.zoomIn': 'Увеличить',
|
'npcs.zoomIn': 'Увеличить',
|
||||||
@@ -658,6 +659,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'top.backToProjects': 'Back to projects',
|
'top.backToProjects': 'Back to projects',
|
||||||
'top.appVersion': 'App version',
|
'top.appVersion': 'App version',
|
||||||
'top.run': 'Run',
|
'top.run': 'Run',
|
||||||
|
'top.launching': 'Starting…',
|
||||||
'top.afterLicense': 'Available after license activation',
|
'top.afterLicense': 'Available after license activation',
|
||||||
'top.setStartScene': 'Set a start scene on the graph (right‑click a node)',
|
'top.setStartScene': 'Set a start scene on the graph (right‑click a node)',
|
||||||
'top.runHelpAria': 'How to enable the Run button',
|
'top.runHelpAria': 'How to enable the Run button',
|
||||||
@@ -977,7 +979,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'npcs.graphZoomOut': 'Zoom out',
|
'npcs.graphZoomOut': 'Zoom out',
|
||||||
'npcs.graphFitAll': 'Fit view',
|
'npcs.graphFitAll': 'Fit view',
|
||||||
'npcs.windowEmpty': 'Add NPCs in the editor.',
|
'npcs.windowEmpty': 'Add NPCs in the editor.',
|
||||||
'npcs.selectToShow': 'Select characters in the list — they will appear on screen.',
|
'npcs.selectToShow': 'Select a character in the list — they will appear on screen.',
|
||||||
'npcs.closeOverlay': 'Close all',
|
'npcs.closeOverlay': 'Close all',
|
||||||
'npcs.rotateOverlay': 'Rotate',
|
'npcs.rotateOverlay': 'Rotate',
|
||||||
'npcs.zoomIn': 'Zoom in',
|
'npcs.zoomIn': 'Zoom in',
|
||||||
|
|||||||
@@ -214,13 +214,13 @@ export function NpcsApp() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className={styles.page}>
|
||||||
{hasActive ? (
|
|
||||||
<div className={styles.toolbar}>
|
<div className={styles.toolbar}>
|
||||||
<div className={styles.toolbarRow}>
|
<div className={styles.toolbarRow}>
|
||||||
<Button
|
<Button
|
||||||
title={t('npcs.closeOverlay')}
|
title={t('npcs.closeOverlay')}
|
||||||
ariaLabel={t('npcs.closeOverlay')}
|
ariaLabel={t('npcs.closeOverlay')}
|
||||||
tooltipPlacement="bottom"
|
tooltipPlacement="bottom"
|
||||||
|
disabled={!hasActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void overlayApi.dispatch({ kind: 'hide' });
|
void overlayApi.dispatch({ kind: 'hide' });
|
||||||
}}
|
}}
|
||||||
@@ -229,7 +229,6 @@ export function NpcsApp() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<div className={styles.detail}>
|
<div className={styles.detail}>
|
||||||
@@ -264,9 +263,11 @@ export function NpcsApp() {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
|
<div className={styles.detailCard}>
|
||||||
<div className={styles.detailEmpty}>
|
<div className={styles.detailEmpty}>
|
||||||
{npcs.length === 0 ? t('npcs.windowEmpty') : t('npcs.selectToShow')}
|
{npcs.length === 0 ? t('npcs.windowEmpty') : t('npcs.selectToShow')}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -342,11 +342,6 @@ function NpcAvatarFrame({
|
|||||||
src={url}
|
src={url}
|
||||||
alt=""
|
alt=""
|
||||||
draggable={false}
|
draggable={false}
|
||||||
style={{
|
|
||||||
width: w,
|
|
||||||
height: h,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
}}
|
|
||||||
onLoad={(e) => {
|
onLoad={(e) => {
|
||||||
const img = e.currentTarget;
|
const img = e.currentTarget;
|
||||||
setNatural({ w: img.naturalWidth || 1, h: img.naturalHeight || 1 });
|
setNatural({ w: img.naturalWidth || 1, h: img.naturalHeight || 1 });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export type EffectToolType =
|
export type EffectToolType =
|
||||||
|
| 'none'
|
||||||
| 'fog'
|
| 'fog'
|
||||||
| 'fire'
|
| 'fire'
|
||||||
| 'rain'
|
| 'rain'
|
||||||
|
|||||||
Reference in New Issue
Block a user