first
This commit is contained in:
35
lib/sprite-bank.ts
Normal file
35
lib/sprite-bank.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export interface Sprite {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type SpritePosition = 'pos1' | 'pos2' | 'pos3' | 'pos4' | 'pos5' | 'pos6' | 'pos7' | 'pos8';
|
||||
|
||||
export const SPRITE_POSITIONS: SpritePosition[] = ['pos1', 'pos2', 'pos3', 'pos4', 'pos5', 'pos6', 'pos7', 'pos8'];
|
||||
|
||||
export const POSITION_MAP: Record<SpritePosition, number> = {
|
||||
pos1: 5,
|
||||
pos2: 17,
|
||||
pos3: 30,
|
||||
pos4: 42,
|
||||
pos5: 55,
|
||||
pos6: 67,
|
||||
pos7: 80,
|
||||
pos8: 92,
|
||||
};
|
||||
|
||||
export const INITIAL_SPRITE_BANK: Record<string, Sprite[]> = {
|
||||
Agnieszka: [
|
||||
{ id: 'happy', name: 'Happy', url: 'https://placehold.co/400x600/ff4d4d/ffffff?text=Agnieszka+Happy' },
|
||||
{ id: 'sad', name: 'Sad', url: 'https://placehold.co/400x600/ff4d4d/ffffff?text=Agnieszka+Sad' },
|
||||
{ id: 'angry', name: 'Angry', url: 'https://placehold.co/400x600/ff4d4d/ffffff?text=Agnieszka+Angry' },
|
||||
{ id: 'neutral', name: 'Neutral', url: 'https://placehold.co/400x600/ff4d4d/ffffff?text=Agnieszka+Neutral' },
|
||||
],
|
||||
Player: [
|
||||
{ id: 'normal', name: 'Normal', url: 'https://placehold.co/400x600/4d79ff/ffffff?text=Player' },
|
||||
],
|
||||
Teacher: [
|
||||
{ id: 'neutral', name: 'Neutral', url: 'https://placehold.co/400x600/4dff4d/ffffff?text=Teacher' },
|
||||
]
|
||||
};
|
||||
217
lib/vn-parser.ts
Normal file
217
lib/vn-parser.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
// --- CONSTANTS ---
|
||||
|
||||
export const CHARACTER_COLORS = [
|
||||
'#ff4d4d', '#4d79ff', '#4dff4d', '#ffff4d', '#ff4dff', '#4dffff',
|
||||
'#ffa64d', '#a64dff', '#ff8080', '#80ff80', '#8080ff'
|
||||
];
|
||||
|
||||
export const getCharColor = (name: string | null) => {
|
||||
if (!name) return '#cccccc';
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return CHARACTER_COLORS[Math.abs(hash) % CHARACTER_COLORS.length];
|
||||
};
|
||||
|
||||
export const DEFAULT_SCRIPT = `// Define Assets
|
||||
define_sprite Agnieszka wave "https://placehold.co/400x600/ff4d4d/ffffff?text=Agnieszka+Wave"
|
||||
define_sprite Player shock "https://placehold.co/400x600/4d79ff/ffffff?text=Player+Shock"
|
||||
|
||||
// Scene 1: The Rooftop Meeting
|
||||
sprite Agnieszka happy at pos2
|
||||
sprite Player normal at pos7
|
||||
say Agnieszka "To jest najprostszy tekst w tekstboxie!"
|
||||
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 "Że to nadal Agnieszka."
|
||||
say "To jest tekst bez osoby podanej!"
|
||||
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 i"Hello! This is italicized" "And this isn't"
|
||||
say Agnieszka u"This is underlined" "And this isn't."
|
||||
say Agnieszka s"This is crossed out" "And this isn't."
|
||||
say Agnieszka "This line is instant" w"This line shows up after 1s"
|
||||
say Agnieszka "This line is instant" w"This line shows up after 3s"(w:3s)
|
||||
say Agnieszka "This line is default colored" c"This line isn't"(c:green)
|
||||
say Agnieszka "This line is default font" f"This line isn't"(f:TimesNewRoman)
|
||||
wait 5s skippable
|
||||
sprite Agnieszka happy at pos5
|
||||
set AGNIESZKA_MET true
|
||||
fire ROOFTOP_SCENE_START
|
||||
say Agnieszka "This is instant" click "Gotta click to read this one"
|
||||
say Agnieszka "This line is instant" clicknoskip "Can't even autoclick this one."
|
||||
nextfile scena_koncowa`;
|
||||
|
||||
// --- TYPES ---
|
||||
|
||||
export interface Modifier {
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
underline: boolean;
|
||||
strikethrough: boolean;
|
||||
wait: boolean;
|
||||
waitDuration: string | null;
|
||||
color: string | null;
|
||||
font: string | null;
|
||||
speed: string | null;
|
||||
clickLock: string | null;
|
||||
click: boolean;
|
||||
clicknoskip: boolean;
|
||||
inlineBranch: any[];
|
||||
}
|
||||
|
||||
export interface Segment {
|
||||
text: string;
|
||||
modifiers: Modifier;
|
||||
}
|
||||
|
||||
export interface Statement {
|
||||
type: 'say' | 'wait' | 'sprite' | 'set' | 'fire' | 'nextfile' | 'comment';
|
||||
character?: string | null;
|
||||
segments?: Segment[];
|
||||
continuations?: Segment[];
|
||||
duration?: string;
|
||||
skippable?: boolean;
|
||||
emotion?: string;
|
||||
variable?: string;
|
||||
value?: string;
|
||||
event?: string;
|
||||
args?: string[];
|
||||
filename?: string;
|
||||
text?: string;
|
||||
lineNumber: number;
|
||||
}
|
||||
|
||||
// --- PARSER ---
|
||||
|
||||
export function parseScript(text: string): Statement[] {
|
||||
const lines = text.split('\n');
|
||||
const statements: Statement[] = [];
|
||||
let currentSay: Statement | null = null;
|
||||
|
||||
const parseSegments = (lineText: string): Segment[] => {
|
||||
const segments: Segment[] = [];
|
||||
const segmentRegex = /([biuswcftn]|click|clicknoskip)?"([^"]*)"(\(([^)]+)\))?/g;
|
||||
let match;
|
||||
while ((match = segmentRegex.exec(lineText)) !== null) {
|
||||
const [full, mod, textValue, fullParens, params] = match;
|
||||
const modifiers: Modifier = {
|
||||
bold: mod === 'b',
|
||||
italic: mod === 'i',
|
||||
underline: mod === 'u',
|
||||
strikethrough: mod === 's',
|
||||
wait: mod === 'w',
|
||||
waitDuration: mod === 'w' ? (params?.match(/w:([\d.s]+)/)?.[1] || '1s') : null,
|
||||
color: mod === 'c' ? (params?.match(/c:([^)]+)/)?.[1] || null) : null,
|
||||
font: mod === 'f' ? (params?.match(/f:([^)]+)/)?.[1] || null) : null,
|
||||
speed: mod === 't' ? (params?.match(/t:([^)]+)/)?.[1] || null) : null,
|
||||
clickLock: mod === 'n' ? (params?.match(/n:([^)]+)/)?.[1] || null) : null,
|
||||
click: mod === 'click',
|
||||
clicknoskip: mod === 'clicknoskip',
|
||||
inlineBranch: []
|
||||
};
|
||||
segments.push({ text: textValue, modifiers });
|
||||
}
|
||||
return segments;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i].trim();
|
||||
const originalLineNumber = i + 1;
|
||||
|
||||
if (!line || line.startsWith('//')) {
|
||||
statements.push({ type: 'comment', text: line, lineNumber: originalLineNumber });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('say ') || line.startsWith('"')) {
|
||||
if (line.startsWith('say ')) {
|
||||
let character: string | null = null;
|
||||
const afterSay = line.slice(4).trim();
|
||||
let segmentPart = afterSay;
|
||||
|
||||
if (afterSay.startsWith('"')) {
|
||||
character = null;
|
||||
} else {
|
||||
const firstQuoteIndex = afterSay.indexOf('"');
|
||||
if (firstQuoteIndex !== -1) {
|
||||
character = afterSay.substring(0, firstQuoteIndex).trim();
|
||||
segmentPart = afterSay.substring(firstQuoteIndex);
|
||||
} else {
|
||||
character = afterSay;
|
||||
segmentPart = "";
|
||||
}
|
||||
}
|
||||
|
||||
currentSay = {
|
||||
type: 'say',
|
||||
character,
|
||||
segments: parseSegments(segmentPart),
|
||||
continuations: [],
|
||||
lineNumber: originalLineNumber
|
||||
};
|
||||
statements.push(currentSay);
|
||||
} else if (line.startsWith('"') && currentSay) {
|
||||
currentSay.continuations?.push(...parseSegments(line));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const words = line.split(/\s+/);
|
||||
let cmd = words[0];
|
||||
|
||||
if (cmd === 'wait') {
|
||||
statements.push({
|
||||
type: 'wait',
|
||||
duration: words[1],
|
||||
skippable: words.includes('skippable'),
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
} else if (cmd === 'define_sprite') {
|
||||
const char = words[1];
|
||||
const id = words[2];
|
||||
const url = words[3]?.replace(/^"(.*)"$/, '$1');
|
||||
statements.push({
|
||||
type: 'comment',
|
||||
text: `DEF_SPRITE ${char} ${id} ${url}`,
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
} else if (cmd === 'sprite') {
|
||||
const char = words[1];
|
||||
const emotion = words[2];
|
||||
const hasAt = words.indexOf('at');
|
||||
const position = hasAt !== -1 ? words[hasAt + 1] : 'pos5';
|
||||
statements.push({
|
||||
type: 'sprite',
|
||||
character: char,
|
||||
emotion: emotion,
|
||||
value: position,
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
} else if (cmd === 'set') {
|
||||
statements.push({
|
||||
type: 'set',
|
||||
variable: words[1],
|
||||
value: words.slice(2).join(' '),
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
} else if (cmd === 'fire') {
|
||||
statements.push({
|
||||
type: 'fire',
|
||||
event: words[1],
|
||||
args: words.slice(2),
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
} else if (cmd === 'nextfile') {
|
||||
statements.push({
|
||||
type: 'nextfile',
|
||||
filename: words[1],
|
||||
lineNumber: originalLineNumber
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
Reference in New Issue
Block a user