add choice mechanic

This commit is contained in:
Stanislaw Dzioba
2026-03-23 19:44:41 +01:00
parent 1373e1e128
commit 86edc3bf88
8 changed files with 300 additions and 136 deletions

View File

@@ -7,6 +7,9 @@ import { VNEditor } from '@/components/VNEditor';
import { VNPreview } from '@/components/VNPreview';
import { INITIAL_SPRITE_BANK, Sprite } from '@/lib/sprite-bank';
const STORAGE_KEY_SCRIPT = 'vn_editor_script';
const STORAGE_KEY_FILENAME = 'vn_editor_filename';
export default function VisualNovelEditorPage() {
const [scriptText, setScriptText] = useState(DEFAULT_SCRIPT);
const [fileName, setFileName] = useState("scene_01.vns");
@@ -16,10 +19,29 @@ export default function VisualNovelEditorPage() {
const [isParsing, setIsParsing] = useState(false);
const editorRef = useRef<HTMLTextAreaElement>(null);
// Load from LocalStorage
useEffect(() => {
const savedScript = localStorage.getItem(STORAGE_KEY_SCRIPT);
const savedFileName = localStorage.getItem(STORAGE_KEY_FILENAME);
if (savedScript) setScriptText(savedScript);
if (savedFileName) setFileName(savedFileName);
}, []);
// Save to LocalStorage
useEffect(() => {
localStorage.setItem(STORAGE_KEY_SCRIPT, scriptText);
localStorage.setItem(STORAGE_KEY_FILENAME, fileName);
}, [scriptText, fileName]);
const updateSpriteBank = (newBank: Record<string, Sprite[]>) => {
setSpriteBank(newBank);
};
const addSpriteToScript = (char: string, id: string, url: string) => {
// Add to the top of the script or after existing defines
setScriptText(prev => `define_sprite ${char} ${id} "${url}"\n${prev}`);
};
// Parse Debounce
useEffect(() => {
const timer = setTimeout(() => {
@@ -34,7 +56,7 @@ export default function VisualNovelEditorPage() {
const parts = s.text.split(' ');
const char = parts[1];
const id = parts[2];
const url = parts[3];
const url = parts[3]?.replace(/^"(.*)"$/, '$1');
if (char && id && url) {
if (!newScriptBank[char]) newScriptBank[char] = [];
if (!newScriptBank[char].some(sp => sp.id === id)) {
@@ -68,12 +90,23 @@ export default function VisualNovelEditorPage() {
}, [scriptText]);
const previewStatements = useMemo(() =>
statements.filter(s => s.type === 'say'),
statements.filter(s => s.type === 'say' || s.type === 'choice'),
[statements]
);
const currentStatement = previewStatements[currentSayIndex] || null;
const handleChoiceSelect = (jumpLabel: string) => {
const labelIdx = statements.findIndex(s => s.type === 'label' && s.label === jumpLabel);
if (labelIdx !== -1) {
// Find the first 'previewable' statement at or after the label
const nextPreviewIdx = previewStatements.findIndex(s => s.lineNumber >= statements[labelIdx].lineNumber);
if (nextPreviewIdx !== -1) {
setCurrentSayIndex(nextPreviewIdx);
}
}
};
const handleSave = () => {
const blob = new Blob([scriptText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
@@ -125,6 +158,7 @@ export default function VisualNovelEditorPage() {
isParsing={isParsing}
spriteBank={spriteBank}
onUpdateSpriteBank={updateSpriteBank}
onAddSpriteToScript={addSpriteToScript}
/>
<div className="flex-1 flex flex-col">
@@ -166,12 +200,13 @@ export default function VisualNovelEditorPage() {
onNext={() => setCurrentSayIndex(prev => Math.min(previewStatements.length - 1, prev + 1))}
statements={statements}
spriteBank={spriteBank}
onChoiceSelect={handleChoiceSelect}
/>
</div>
</div>
<style jsx global>{`
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Noto+Serif:ital,wght@0,400;0,700;1,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Noto+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap');
.custom-scrollbar::-webkit-scrollbar {
width: 8px;