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:
Ivan Fontosh
2026-07-25 14:41:27 +08:00
parent fed5674468
commit 9d82e74272
10 changed files with 88 additions and 57 deletions
+22 -25
View File
@@ -23,7 +23,7 @@ import {
PixiEffectsOverlay,
type PixiEffectsOverlayHandle,
} 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 { useEffectsState } from '../shared/effects/useEffectsState';
import { useSceneDarknessState } from '../shared/effects/useSceneDarknessState';
@@ -839,10 +839,16 @@ export function ControlApp() {
const showReturnToMain = isInSideStoryline && mainStoryReturnGraphNodeId !== null;
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);
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 {
const el = brushCursorElRef.current;
const p = cursorPosRef.current;
@@ -850,7 +856,7 @@ export function ControlApp() {
const ps = previewSizeRef.current;
const t = toolRef.current;
if (!el) return;
if (!p) {
if (!p || t.tool === 'none') {
el.style.visibility = 'hidden';
return;
}
@@ -1447,7 +1453,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1483,7 +1489,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1492,7 +1498,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1501,7 +1507,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1510,7 +1516,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1524,9 +1530,7 @@ export function ControlApp() {
iconOnly
title={t('control.lightning')}
ariaLabel={t('control.lightning')}
onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'lightning' } })
}
onClick={() => selectEffectTool('lightning')}
>
<span className={styles.iconGlyph}></span>
</Button>
@@ -1535,7 +1539,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1544,7 +1548,7 @@ export function ControlApp() {
iconOnly
title={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>
</Button>
@@ -1553,9 +1557,7 @@ export function ControlApp() {
iconOnly
title={t('control.darkness')}
ariaLabel={t('control.darkness')}
onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'darkness' } })
}
onClick={() => selectEffectTool('darkness')}
>
<span className={styles.iconGlyph}>🌑</span>
</Button>
@@ -1564,9 +1566,7 @@ export function ControlApp() {
iconOnly
title={t('control.poisonCloud')}
ariaLabel={t('control.poisonCloud')}
onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'poisonCloud' } })
}
onClick={() => selectEffectTool('poisonCloud')}
>
<span className={styles.iconGlyph}></span>
</Button>
@@ -1575,9 +1575,7 @@ export function ControlApp() {
iconOnly
title={t('control.explosion')}
ariaLabel={t('control.explosion')}
onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'explosion' } })
}
onClick={() => selectEffectTool('explosion')}
>
<span className={styles.iconGlyph}>💥</span>
</Button>
@@ -1592,9 +1590,7 @@ export function ControlApp() {
iconOnly
title={t('control.explorerBrush')}
ariaLabel={t('control.explorerBrush')}
onClick={() =>
void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: 'exploreBrush' } })
}
onClick={() => selectEffectTool('exploreBrush')}
>
<span className={styles.iconGlyph}>🔦</span>
</Button>
@@ -1772,6 +1768,7 @@ export function ControlApp() {
return;
}
if (e.button !== 0) return;
if (tool.tool === 'none') return;
const p = toNPoint(e);
if (!p) return;
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.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("title={t('control.clearEffects')}"));
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));