218 lines
6.7 KiB
TypeScript
218 lines
6.7 KiB
TypeScript
// --- 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;
|
|
}
|