space-game001/dialogEditor/src/components/PlayMode/PlayModeOverlay.tsx
2026-06-05 21:17:51 +03:00

171 lines
6.2 KiB
TypeScript

import { useEffect } from 'react';
import { useDialogueStore } from '../../store/dialogueStore';
import { ConditionClause } from '../../types/dialogue';
import { MAIN_CHARACTER } from '../../constants/characters';
import styles from './PlayModeOverlay.module.css';
function evalConditions(clauses: ConditionClause[], flags: Record<string, number | string>): boolean {
return clauses.every(c => {
const actual = flags[c.flag] ?? 0;
switch (c.op) {
case 'Equals': return actual == c.value;
case 'NotEquals': return actual != c.value;
case 'GreaterThan': return Number(actual) > Number(c.value);
case 'LessThan': return Number(actual) < Number(c.value);
}
});
}
export function PlayModeOverlay() {
const { file, selectedDialogueId, playState, advancePlay, setPlayFlag, stopPlay } = useDialogueStore();
const dialogue = file?.dialogues.find(d => d.id === selectedDialogueId);
const node = dialogue?.nodes.find(n => n.id === playState?.currentNodeId);
// Auto-advance for Condition and SetFlag
useEffect(() => {
if (!node || !playState) return;
if (node.type === 'Condition') {
const timer = setTimeout(() => {
const passes = evalConditions(node.conditions, playState.flags);
advancePlay(passes ? node.trueNext : node.falseNext);
}, 300);
return () => clearTimeout(timer);
}
if (node.type === 'SetFlag') {
const timer = setTimeout(() => {
for (const effect of node.effects) {
setPlayFlag(effect.flag, effect.value);
}
advancePlay(node.next);
}, 100);
return () => clearTimeout(timer);
}
}, [node?.id, node?.type]);
if (!playState || !node) return null;
const portraitPath = node.type === 'Line' || node.type === 'Choice'
? `../${node.portrait}`
: null;
return (
<div className={styles.overlay} onClick={e => e.stopPropagation()}>
<div className={styles.dialog}>
<button className={styles.closeBtn} onClick={stopPlay} title="Exit play mode"></button>
{node.type === 'Line' && (
<>
<div className={[
styles.speakerRow,
node.speaker === MAIN_CHARACTER ? styles.speakerRowMain : styles.speakerRowOther,
].join(' ')}>
<div className={[
styles.portraitBox,
node.speaker === MAIN_CHARACTER ? styles.portraitMain : styles.portraitOther,
].join(' ')}>
<img
src={portraitPath!}
alt={node.speaker}
className={styles.portrait}
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<span className={styles.portraitFallback}>{node.speaker[0]}</span>
</div>
<span className={[
styles.speakerName,
node.speaker === MAIN_CHARACTER ? styles.speakerMain : styles.speakerOther,
].join(' ')}>{node.speaker}</span>
</div>
<div className={[
styles.textBox,
node.speaker === MAIN_CHARACTER ? styles.textBoxMain : styles.textBoxOther,
].join(' ')}>{node.text}</div>
{node.next ? (
<button className={styles.nextBtn} onClick={() => advancePlay(node.next)}>
Next
</button>
) : (
<div className={styles.noNextWarning}> No next node set</div>
)}
</>
)}
{node.type === 'Choice' && (
<>
<div className={styles.speakerRow}>
<div className={styles.portraitBox}>
<img
src={portraitPath!}
alt={node.speaker}
className={styles.portrait}
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<span className={styles.portraitFallback}>{node.speaker[0]}</span>
</div>
<span className={styles.speakerName}>{node.speaker}</span>
</div>
{node.text && <div className={styles.textBox}>{node.text}</div>}
<div className={styles.choiceList}>
{node.choices.map(choice => (
<button
key={choice.id}
className={[styles.choiceBtn, choice.kind === 'Main' ? styles.choiceMain : styles.choiceOptional].join(' ')}
onClick={() => advancePlay(choice.next)}
>
{choice.text || '(empty choice)'}
</button>
))}
</div>
</>
)}
{node.type === 'Condition' && (
<div className={styles.autoAdvance}>
<div className={styles.conditionInfo}>Evaluating condition...</div>
<div className={styles.conditionClauses}>
{node.conditions.map((c, i) => (
<span key={i} className={styles.clausePill}>{c.flag} {c.op} {c.value}</span>
))}
</div>
</div>
)}
{node.type === 'SetFlag' && (
<div className={styles.autoAdvance}>
<div className={styles.conditionInfo}>Setting flags...</div>
</div>
)}
{node.type === 'CutsceneStart' && (
<>
<div className={styles.cutsceneBox}>
<span className={styles.cutsceneLabel}>Cutscene:</span>
<span className={styles.cutsceneId}>{node.cutsceneId}</span>
</div>
<button className={styles.nextBtn} onClick={() => advancePlay(node.next)}>
Continue
</button>
</>
)}
{node.type === 'End' && (
<div className={styles.endBox}>
<div className={styles.endMsg}>Dialogue ended.</div>
<button className={styles.nextBtn} onClick={stopPlay}>Close</button>
</div>
)}
<div className={styles.debugBar}>
<span className={styles.debugLabel}>node: {node.id}</span>
{Object.entries(playState.flags).map(([k, v]) => (
<span key={k} className={styles.debugFlagPill}>{k}={v}</span>
))}
</div>
</div>
</div>
);
}