add choice mechanic
This commit is contained in:
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Stage 1: Install dependencies
|
||||||
|
FROM node:20-alpine AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Stage 2: Build the application
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 3: Production image
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
# server.js is created by next build from the standalone output
|
||||||
|
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||||
|
CMD ["node", "server.js"]
|
||||||
53
GEMINI.md
Normal file
53
GEMINI.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Visual Novel Dating Sim Editor
|
||||||
|
|
||||||
|
A web-based editor for creating and previewing Visual Novel scripts with real-time feedback.
|
||||||
|
|
||||||
|
## 🏗 Architecture
|
||||||
|
|
||||||
|
- **Frontend**: Next.js (TypeScript) + Tailwind CSS.
|
||||||
|
- **State Management**: React `useState` and `useEffect` in `app/page.tsx`.
|
||||||
|
- **Script Engine**: Custom regex-based parser in `lib/vn-parser.ts`.
|
||||||
|
- **Renderer**: Dynamic React component in `components/VNPreview.tsx` that reconstructs the scene state.
|
||||||
|
- **Persistence**:
|
||||||
|
- Automatic `localStorage` sync for current session.
|
||||||
|
- Script export/import as `.vns` files.
|
||||||
|
- Inline Base64 image embedding for portable project files.
|
||||||
|
|
||||||
|
## 📝 Script Syntax
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
- `say Character "Dialogue"`: Display dialogue for a character.
|
||||||
|
- `say "Dialogue"`: Narrator text.
|
||||||
|
- `sprite Char Emotion at Position`: Place a character on screen.
|
||||||
|
- Positions: `pos1` to `pos8`.
|
||||||
|
- `wait 2s`: Pause for duration (can add `skippable`).
|
||||||
|
- `label Name`: Define a jump point.
|
||||||
|
- `choice "Text" jump:Label`: Create a branching choice.
|
||||||
|
- `define_sprite Char ID "URL"`: Register a sprite asset.
|
||||||
|
|
||||||
|
### Rich Text Modifiers
|
||||||
|
- `b"Bold"`
|
||||||
|
- `i"Italic"`
|
||||||
|
- `u"Underline"`
|
||||||
|
- `c"Red"(c:red)`
|
||||||
|
- `w"Wait"(w:1s)`
|
||||||
|
|
||||||
|
## 🚀 Production Deployment
|
||||||
|
|
||||||
|
### Docker (Recommended)
|
||||||
|
The project is optimized for Docker using Next.js standalone output.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the image
|
||||||
|
docker build -t vn-editor .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 vn-editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Build
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
41
app/page.tsx
41
app/page.tsx
@@ -7,6 +7,9 @@ import { VNEditor } from '@/components/VNEditor';
|
|||||||
import { VNPreview } from '@/components/VNPreview';
|
import { VNPreview } from '@/components/VNPreview';
|
||||||
import { INITIAL_SPRITE_BANK, Sprite } from '@/lib/sprite-bank';
|
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() {
|
export default function VisualNovelEditorPage() {
|
||||||
const [scriptText, setScriptText] = useState(DEFAULT_SCRIPT);
|
const [scriptText, setScriptText] = useState(DEFAULT_SCRIPT);
|
||||||
const [fileName, setFileName] = useState("scene_01.vns");
|
const [fileName, setFileName] = useState("scene_01.vns");
|
||||||
@@ -16,10 +19,29 @@ export default function VisualNovelEditorPage() {
|
|||||||
const [isParsing, setIsParsing] = useState(false);
|
const [isParsing, setIsParsing] = useState(false);
|
||||||
const editorRef = useRef<HTMLTextAreaElement>(null);
|
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[]>) => {
|
const updateSpriteBank = (newBank: Record<string, Sprite[]>) => {
|
||||||
setSpriteBank(newBank);
|
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
|
// Parse Debounce
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -34,7 +56,7 @@ export default function VisualNovelEditorPage() {
|
|||||||
const parts = s.text.split(' ');
|
const parts = s.text.split(' ');
|
||||||
const char = parts[1];
|
const char = parts[1];
|
||||||
const id = parts[2];
|
const id = parts[2];
|
||||||
const url = parts[3];
|
const url = parts[3]?.replace(/^"(.*)"$/, '$1');
|
||||||
if (char && id && url) {
|
if (char && id && url) {
|
||||||
if (!newScriptBank[char]) newScriptBank[char] = [];
|
if (!newScriptBank[char]) newScriptBank[char] = [];
|
||||||
if (!newScriptBank[char].some(sp => sp.id === id)) {
|
if (!newScriptBank[char].some(sp => sp.id === id)) {
|
||||||
@@ -68,12 +90,23 @@ export default function VisualNovelEditorPage() {
|
|||||||
}, [scriptText]);
|
}, [scriptText]);
|
||||||
|
|
||||||
const previewStatements = useMemo(() =>
|
const previewStatements = useMemo(() =>
|
||||||
statements.filter(s => s.type === 'say'),
|
statements.filter(s => s.type === 'say' || s.type === 'choice'),
|
||||||
[statements]
|
[statements]
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentStatement = previewStatements[currentSayIndex] || null;
|
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 handleSave = () => {
|
||||||
const blob = new Blob([scriptText], { type: 'text/plain' });
|
const blob = new Blob([scriptText], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -125,6 +158,7 @@ export default function VisualNovelEditorPage() {
|
|||||||
isParsing={isParsing}
|
isParsing={isParsing}
|
||||||
spriteBank={spriteBank}
|
spriteBank={spriteBank}
|
||||||
onUpdateSpriteBank={updateSpriteBank}
|
onUpdateSpriteBank={updateSpriteBank}
|
||||||
|
onAddSpriteToScript={addSpriteToScript}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<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))}
|
onNext={() => setCurrentSayIndex(prev => Math.min(previewStatements.length - 1, prev + 1))}
|
||||||
statements={statements}
|
statements={statements}
|
||||||
spriteBank={spriteBank}
|
spriteBank={spriteBank}
|
||||||
|
onChoiceSelect={handleChoiceSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style jsx global>{`
|
<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 {
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Statement } from '@/lib/vn-parser';
|
import { Statement } from '@/lib/vn-parser';
|
||||||
import { Sprite } from '@/lib/sprite-bank';
|
import { Sprite } from '@/lib/sprite-bank';
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ interface VNOutlineProps {
|
|||||||
isParsing: boolean;
|
isParsing: boolean;
|
||||||
spriteBank: Record<string, Sprite[]>;
|
spriteBank: Record<string, Sprite[]>;
|
||||||
onUpdateSpriteBank: (newBank: Record<string, Sprite[]>) => void;
|
onUpdateSpriteBank: (newBank: Record<string, Sprite[]>) => void;
|
||||||
|
onAddSpriteToScript: (char: string, id: string, url: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VNOutline: React.FC<VNOutlineProps> = ({
|
export const VNOutline: React.FC<VNOutlineProps> = ({
|
||||||
@@ -17,10 +18,13 @@ export const VNOutline: React.FC<VNOutlineProps> = ({
|
|||||||
onSelectLine,
|
onSelectLine,
|
||||||
isParsing,
|
isParsing,
|
||||||
spriteBank,
|
spriteBank,
|
||||||
onUpdateSpriteBank
|
onUpdateSpriteBank,
|
||||||
|
onAddSpriteToScript
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<'outline' | 'sprites'>('outline');
|
const [activeTab, setActiveTab] = useState<'outline' | 'sprites'>('outline');
|
||||||
const [newCharName, setNewCharName] = useState('');
|
const [newCharName, setNewCharName] = useState('');
|
||||||
|
const [uploadingFor, setUploadingFor] = useState<string | null>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const addCharacter = () => {
|
const addCharacter = () => {
|
||||||
if (!newCharName || spriteBank[newCharName]) return;
|
if (!newCharName || spriteBank[newCharName]) return;
|
||||||
@@ -33,14 +37,39 @@ export const VNOutline: React.FC<VNOutlineProps> = ({
|
|||||||
onUpdateSpriteBank(rest);
|
onUpdateSpriteBank(rest);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSprite = (charName: string) => {
|
const addSpriteUrl = (charName: string) => {
|
||||||
const id = prompt('Sprite ID (e.g. happy):');
|
const id = prompt('Sprite ID (e.g. happy):');
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
const url = prompt('Sprite Image URL:');
|
const url = prompt('Sprite Image URL:');
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
const newSprites = [...spriteBank[charName], { id, name: id, url }];
|
onAddSpriteToScript(charName, id, url);
|
||||||
onUpdateSpriteBank({ ...spriteBank, [charName]: newSprites });
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file || !uploadingFor) return;
|
||||||
|
|
||||||
|
const id = prompt('Sprite ID (e.g. happy):');
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const url = e.target?.result as string;
|
||||||
|
if (url) {
|
||||||
|
onAddSpriteToScript(uploadingFor, id, url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
setUploadingFor(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerFileUpload = (charName: string) => {
|
||||||
|
setUploadingFor(charName);
|
||||||
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSprite = (charName: string, spriteId: string) => {
|
const removeSprite = (charName: string, spriteId: string) => {
|
||||||
@@ -132,15 +161,16 @@ export const VNOutline: React.FC<VNOutlineProps> = ({
|
|||||||
{charName}
|
{charName}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button onClick={() => addSprite(charName)} className="text-[8px] text-gray-400 hover:text-cyan-400 uppercase font-bold tracking-tighter">Add Sprite</button>
|
<button onClick={() => addSpriteUrl(charName)} className="text-[8px] text-gray-400 hover:text-cyan-400 uppercase font-bold tracking-tighter">URL</button>
|
||||||
|
<button onClick={() => triggerFileUpload(charName)} className="text-[8px] text-gray-400 hover:text-cyan-400 uppercase font-bold tracking-tighter">Upload</button>
|
||||||
<button onClick={() => removeCharacter(charName)} className="text-[8px] text-gray-400 hover:text-red-400 uppercase font-bold tracking-tighter">Delete</button>
|
<button onClick={() => removeCharacter(charName)} className="text-[8px] text-gray-400 hover:text-red-400 uppercase font-bold tracking-tighter">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{sprites.map(sprite => (
|
{sprites.map(sprite => (
|
||||||
<div key={sprite.id} className="group relative">
|
<div key={sprite.id} className="group relative">
|
||||||
<div className="aspect-[2/3] bg-gray-900 rounded border border-gray-800 overflow-hidden">
|
<div className="aspect-[2/3] bg-gray-900 rounded border border-gray-800 overflow-hidden p-1">
|
||||||
<img src={sprite.url} alt={sprite.name} className="w-full h-full object-cover opacity-60 group-hover:opacity-100 transition-opacity" />
|
<img src={sprite.url} alt={sprite.name} className="w-full h-full object-contain opacity-60 group-hover:opacity-100 transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute bottom-0 inset-x-0 bg-black/80 p-1 text-[8px] font-bold flex justify-between items-center group-hover:bg-cyan-950/90">
|
<div className="absolute bottom-0 inset-x-0 bg-black/80 p-1 text-[8px] font-bold flex justify-between items-center group-hover:bg-cyan-950/90">
|
||||||
<span className="truncate flex-1">{sprite.id}</span>
|
<span className="truncate flex-1">{sprite.id}</span>
|
||||||
@@ -156,6 +186,13 @@ export const VNOutline: React.FC<VNOutlineProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface VNPreviewProps {
|
|||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
statements: Statement[];
|
statements: Statement[];
|
||||||
spriteBank: Record<string, Sprite[]>;
|
spriteBank: Record<string, Sprite[]>;
|
||||||
|
onChoiceSelect?: (jumpLabel: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VNPreview: React.FC<VNPreviewProps> = ({
|
export const VNPreview: React.FC<VNPreviewProps> = ({
|
||||||
@@ -21,15 +22,20 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
onNext,
|
onNext,
|
||||||
statements,
|
statements,
|
||||||
spriteBank,
|
spriteBank,
|
||||||
|
onChoiceSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// Track state of all characters on scene at the current line
|
// Track state of all characters on scene and the current label
|
||||||
const activeSprites: Record<
|
const activeSprites: Record<
|
||||||
string,
|
string,
|
||||||
{ emotion: string; pos: SpritePosition }
|
{ emotion: string; pos: SpritePosition }
|
||||||
> = {};
|
> = {};
|
||||||
|
let currentLabel: string | null = null;
|
||||||
|
|
||||||
statements.forEach((s) => {
|
statements.forEach((s) => {
|
||||||
if (s.lineNumber <= (currentStatement?.lineNumber || 999999)) {
|
if (s.lineNumber <= (currentStatement?.lineNumber || 999999)) {
|
||||||
|
if (s.type === "label") {
|
||||||
|
currentLabel = s.label || null;
|
||||||
|
}
|
||||||
if (s.type === "sprite" && s.character) {
|
if (s.type === "sprite" && s.character) {
|
||||||
activeSprites[s.character] = {
|
activeSprites[s.character] = {
|
||||||
emotion: s.emotion || "default",
|
emotion: s.emotion || "default",
|
||||||
@@ -46,7 +52,25 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDialogueBox = () => {
|
const renderDialogueBox = () => {
|
||||||
if (!currentStatement || currentStatement.type !== "say") return null;
|
if (!currentStatement) return null;
|
||||||
|
|
||||||
|
if (currentStatement.type === "choice") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 items-center">
|
||||||
|
{currentStatement.choices?.map((choice, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={() => onChoiceSelect?.(choice.jumpLabel)}
|
||||||
|
className="w-full max-w-lg bg-black/80 hover:bg-cyan-900/80 border border-cyan-500/50 hover:border-cyan-400 p-4 rounded-md text-cyan-100 font-bold transition-all transform hover:scale-[1.02] active:scale-[0.98] shadow-lg backdrop-blur-md"
|
||||||
|
>
|
||||||
|
{choice.text}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStatement.type !== "say") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -64,7 +88,7 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-black/60 border border-white/10 p-6 pt-8 rounded-lg rounded-tl-none backdrop-blur-md shadow-2xl min-h-[140px]">
|
<div className="bg-black/60 border border-white/10 p-6 pt-8 rounded-lg rounded-tl-none backdrop-blur-md shadow-2xl min-h-[140px]">
|
||||||
<div className="text-xl leading-relaxed text-gray-100 italic font-serif">
|
<div className="text-xl leading-relaxed text-gray-100 font-serif">
|
||||||
{[
|
{[
|
||||||
...(currentStatement.segments || []),
|
...(currentStatement.segments || []),
|
||||||
...(currentStatement.continuations || []),
|
...(currentStatement.continuations || []),
|
||||||
@@ -87,7 +111,7 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
{seg.modifiers.wait && <Icons.Wait />}
|
{seg.modifiers.wait && <Icons.Wait />}
|
||||||
{seg.modifiers.click && <Icons.Click />}
|
{seg.modifiers.click && <Icons.Click />}
|
||||||
{seg.modifiers.clickLock && <Icons.Lock />}
|
{seg.modifiers.clickLock && <Icons.Lock />}
|
||||||
{seg.text}{" "}
|
{seg.text}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -99,10 +123,15 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 bg-[#0b0e14] p-8 flex flex-col items-center justify-center relative">
|
<div className="flex-1 bg-[#0b0e14] p-8 flex flex-col items-center justify-center relative">
|
||||||
<div className="w-full max-w-4xl aspect-video bg-gradient-to-b from-gray-900 to-black rounded-lg overflow-hidden border border-gray-800 shadow-2xl relative group">
|
<div className="w-full max-w-4xl aspect-video bg-gradient-to-b from-gray-900 to-black rounded-lg overflow-hidden border border-gray-800 shadow-2xl relative group">
|
||||||
<div className="absolute top-4 left-6 z-20 flex gap-2">
|
<div className="absolute top-4 left-6 z-50 flex gap-2 items-center">
|
||||||
<span className="text-[10px] tracking-[0.2em] uppercase text-gray-500 font-bold bg-black/40 px-3 py-1 rounded backdrop-blur border border-white/5">
|
<span className="text-[10px] tracking-[0.2em] uppercase text-gray-500 font-bold bg-black/40 px-3 py-1 rounded backdrop-blur border border-white/5">
|
||||||
PREVIEW | {currentStatement?.character || "NARRATOR"}
|
PREVIEW | {currentStatement?.character || "NARRATOR"}
|
||||||
</span>
|
</span>
|
||||||
|
{currentLabel && (
|
||||||
|
<span className="text-[9px] tracking-widest uppercase text-cyan-400 font-bold bg-cyan-950/40 px-3 py-1 rounded backdrop-blur border border-cyan-500/20">
|
||||||
|
SCENE: {currentLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-0 pointer-events-none">
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
@@ -112,7 +141,7 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
const isSmallPos = data.pos === "pos7" || data.pos === "pos8";
|
const isSmallPos = data.pos === "pos7" || data.pos === "pos8";
|
||||||
const baseScale = isSmallPos ? 0.8 : 1.0;
|
const baseScale = isSmallPos ? 0.8 : 1.0;
|
||||||
const finalScale = isSpeaking ? baseScale * 1.05 : baseScale;
|
const finalScale = isSpeaking ? baseScale * 1.05 : baseScale;
|
||||||
const translateY = isSpeaking ? "1rem" : "2rem";
|
const translateY = isSpeaking ? "0rem" : "0.5rem";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -122,7 +151,7 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
zIndex: isSpeaking ? 20 : 10,
|
zIndex: isSpeaking ? 20 : 10,
|
||||||
transform: `translateX(-50%) translateY(${translateY}) scale(${finalScale})`,
|
transform: `translateX(-50%) translateY(${translateY}) scale(${finalScale})`,
|
||||||
}}
|
}}
|
||||||
className="absolute bottom-0 h-[85%] transition-all duration-700"
|
className="absolute bottom-0 h-[95%] transition-all duration-700"
|
||||||
>
|
>
|
||||||
{url ? (
|
{url ? (
|
||||||
<img
|
<img
|
||||||
@@ -173,7 +202,7 @@ export const VNPreview: React.FC<VNPreviewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-y-0 right-2 flex items-center z-40">
|
<div className="absolute inset-y-0 right-2 flex items-center z-40">
|
||||||
<button
|
<button
|
||||||
disabled={currentSayIndex >= totalPreviewSteps - 1}
|
disabled={currentSayIndex >= totalPreviewSteps - 1 || currentStatement?.type === 'choice'}
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
className="p-2 text-white/30 hover:text-cyan-400 transition-colors disabled:opacity-0"
|
className="p-2 text-white/30 hover:text-cyan-400 transition-colors disabled:opacity-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,12 +22,23 @@ define_sprite Player shock "https://placehold.co/400x600/4d79ff/ffffff?text=Play
|
|||||||
sprite Agnieszka happy at pos2
|
sprite Agnieszka happy at pos2
|
||||||
sprite Player normal at pos7
|
sprite Player normal at pos7
|
||||||
say Agnieszka "To jest najprostszy tekst w tekstboxie!"
|
say Agnieszka "To jest najprostszy tekst w tekstboxie!"
|
||||||
|
say Agnieszka "Chcesz zobaczyć specjalny efekt?"
|
||||||
|
|
||||||
|
choice "Tak, pokaż mi!" jump:special_effect
|
||||||
|
choice "Nie, przejdźmy dalej" jump:continue_scene
|
||||||
|
|
||||||
|
label special_effect
|
||||||
|
sprite Agnieszka wave at pos3
|
||||||
|
say Agnieszka "Oto on! Pogrubiony tekst: " b"BUM!"
|
||||||
|
say Agnieszka "I teraz wracamy do głównej sceny."
|
||||||
|
|
||||||
|
label continue_scene
|
||||||
|
say Agnieszka "Kontynuujemy naszą rozmowę."
|
||||||
say Agnieszka "To jest najprostszy tekst w tekstboxie!" "Tu jest dużo tekstu."
|
say Agnieszka "To jest najprostszy tekst w tekstboxie!" "Tu jest dużo tekstu."
|
||||||
say "To by było trochę niekomfortowe trzymać w jednej linii." "Ale wszyscy wiedzą."
|
say "To by było trochę niekomfortowe trzymać w jednej linii." "Ale wszyscy wiedzą."
|
||||||
say "Że to nadal Agnieszka."
|
say "Że to nadal Agnieszka."
|
||||||
say "To jest tekst bez osoby podanej!"
|
say "To jest tekst bez osoby podanej!"
|
||||||
sprite Agnieszka wave at pos3
|
sprite Agnieszka wave at pos3
|
||||||
say Agnieszka "Look, I defined this 'wave' sprite right in the script!"
|
|
||||||
say Agnieszka b"To jest pogrubione" "A to nie!" b"P""ierwsza litera tylko"
|
say Agnieszka b"To jest pogrubione" "A to nie!" b"P""ierwsza litera tylko"
|
||||||
say Agnieszka i"Hello! This is italicized" "And this isn't"
|
say Agnieszka i"Hello! This is italicized" "And this isn't"
|
||||||
say Agnieszka u"This is underlined" "And this isn't."
|
say Agnieszka u"This is underlined" "And this isn't."
|
||||||
@@ -67,8 +78,13 @@ export interface Segment {
|
|||||||
modifiers: Modifier;
|
modifiers: Modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChoiceOption {
|
||||||
|
text: string;
|
||||||
|
jumpLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Statement {
|
export interface Statement {
|
||||||
type: 'say' | 'wait' | 'sprite' | 'set' | 'fire' | 'nextfile' | 'comment';
|
type: 'say' | 'wait' | 'sprite' | 'set' | 'fire' | 'nextfile' | 'comment' | 'choice' | 'label';
|
||||||
character?: string | null;
|
character?: string | null;
|
||||||
segments?: Segment[];
|
segments?: Segment[];
|
||||||
continuations?: Segment[];
|
continuations?: Segment[];
|
||||||
@@ -82,6 +98,8 @@ export interface Statement {
|
|||||||
filename?: string;
|
filename?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
|
label?: string;
|
||||||
|
choices?: ChoiceOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PARSER ---
|
// --- PARSER ---
|
||||||
@@ -95,7 +113,23 @@ export function parseScript(text: string): Statement[] {
|
|||||||
const segments: Segment[] = [];
|
const segments: Segment[] = [];
|
||||||
const segmentRegex = /([biuswcftn]|click|clicknoskip)?"([^"]*)"(\(([^)]+)\))?/g;
|
const segmentRegex = /([biuswcftn]|click|clicknoskip)?"([^"]*)"(\(([^)]+)\))?/g;
|
||||||
let match;
|
let match;
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
while ((match = segmentRegex.exec(lineText)) !== null) {
|
while ((match = segmentRegex.exec(lineText)) !== null) {
|
||||||
|
// Add any text between matches (like spaces) as a plain segment
|
||||||
|
const gap = lineText.substring(lastIndex, match.index);
|
||||||
|
if (gap) {
|
||||||
|
segments.push({
|
||||||
|
text: gap,
|
||||||
|
modifiers: {
|
||||||
|
bold: false, italic: false, underline: false, strikethrough: false,
|
||||||
|
wait: false, waitDuration: null, color: null, font: null,
|
||||||
|
speed: null, clickLock: null, click: false, clicknoskip: false,
|
||||||
|
inlineBranch: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const [full, mod, textValue, fullParens, params] = match;
|
const [full, mod, textValue, fullParens, params] = match;
|
||||||
const modifiers: Modifier = {
|
const modifiers: Modifier = {
|
||||||
bold: mod === 'b',
|
bold: mod === 'b',
|
||||||
@@ -113,7 +147,23 @@ export function parseScript(text: string): Statement[] {
|
|||||||
inlineBranch: []
|
inlineBranch: []
|
||||||
};
|
};
|
||||||
segments.push({ text: textValue, modifiers });
|
segments.push({ text: textValue, modifiers });
|
||||||
|
lastIndex = segmentRegex.lastIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add remaining text after the last match
|
||||||
|
const remaining = lineText.substring(lastIndex);
|
||||||
|
if (remaining) {
|
||||||
|
segments.push({
|
||||||
|
text: remaining,
|
||||||
|
modifiers: {
|
||||||
|
bold: false, italic: false, underline: false, strikethrough: false,
|
||||||
|
wait: false, waitDuration: null, color: null, font: null,
|
||||||
|
speed: null, clickLock: null, click: false, clicknoskip: false,
|
||||||
|
inlineBranch: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return segments;
|
return segments;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,10 +185,12 @@ export function parseScript(text: string): Statement[] {
|
|||||||
if (afterSay.startsWith('"')) {
|
if (afterSay.startsWith('"')) {
|
||||||
character = null;
|
character = null;
|
||||||
} else {
|
} else {
|
||||||
const firstQuoteIndex = afterSay.indexOf('"');
|
// Find the start of the first segment.
|
||||||
if (firstQuoteIndex !== -1) {
|
// A segment starts with a quote, or a modifier followed by a quote.
|
||||||
character = afterSay.substring(0, firstQuoteIndex).trim();
|
const segmentMatch = afterSay.match(/([biuswcftn]|click|clicknoskip)?"/);
|
||||||
segmentPart = afterSay.substring(firstQuoteIndex);
|
if (segmentMatch && segmentMatch.index !== undefined) {
|
||||||
|
character = afterSay.substring(0, segmentMatch.index).trim();
|
||||||
|
segmentPart = afterSay.substring(segmentMatch.index);
|
||||||
} else {
|
} else {
|
||||||
character = afterSay;
|
character = afterSay;
|
||||||
segmentPart = "";
|
segmentPart = "";
|
||||||
@@ -159,10 +211,31 @@ export function parseScript(text: string): Statement[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const words = line.split(/\s+/);
|
const words = line.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
||||||
let cmd = words[0];
|
let cmd = words[0];
|
||||||
|
|
||||||
if (cmd === 'wait') {
|
if (cmd === 'label') {
|
||||||
|
statements.push({
|
||||||
|
type: 'label',
|
||||||
|
label: words[1],
|
||||||
|
lineNumber: originalLineNumber
|
||||||
|
});
|
||||||
|
} else if (cmd === 'choice') {
|
||||||
|
const text = words[1]?.replace(/^"(.*)"$/, '$1');
|
||||||
|
const jumpPart = words[2];
|
||||||
|
const jumpLabel = jumpPart?.startsWith('jump:') ? jumpPart.slice(5) : '';
|
||||||
|
|
||||||
|
const lastStatement = statements[statements.length - 1];
|
||||||
|
if (lastStatement?.type === 'choice') {
|
||||||
|
lastStatement.choices?.push({ text, jumpLabel });
|
||||||
|
} else {
|
||||||
|
statements.push({
|
||||||
|
type: 'choice',
|
||||||
|
choices: [{ text, jumpLabel }],
|
||||||
|
lineNumber: originalLineNumber
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (cmd === 'wait') {
|
||||||
statements.push({
|
statements.push({
|
||||||
type: 'wait',
|
type: 'wait',
|
||||||
duration: words[1],
|
duration: words[1],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: 'standalone',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
108
package-lock.json
generated
108
package-lock.json
generated
@@ -598,9 +598,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -617,9 +614,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -636,9 +630,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -655,9 +646,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -674,9 +662,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -693,9 +678,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -712,9 +694,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -731,9 +710,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -750,9 +726,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -775,9 +748,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -800,9 +770,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -825,9 +792,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -850,9 +814,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -875,9 +836,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -900,9 +858,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -925,9 +880,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1137,9 +1089,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1156,9 +1105,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1175,9 +1121,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1194,9 +1137,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1435,9 +1375,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1455,9 +1392,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1475,9 +1409,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1495,9 +1426,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2048,9 +1976,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2065,9 +1990,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2082,9 +2004,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2099,9 +2018,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2116,9 +2032,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2133,9 +2046,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2150,9 +2060,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2167,9 +2074,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4800,9 +4704,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4824,9 +4725,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4848,9 +4746,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -4872,9 +4767,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
Reference in New Issue
Block a user