This commit is contained in:
unknown
2026-03-15 22:41:04 +01:00
commit 1373e1e128
23 changed files with 7823 additions and 0 deletions

35
lib/sprite-bank.ts Normal file
View 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
View 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;
}