UNPKG

yukinovel

Version:

Yukinovel is a simple web visual novel engine.

306 lines (305 loc) 11.3 kB
export class ScriptingEngine { constructor(game) { this.variables = new Map(); this.functions = new Map(); this.stack = []; // Call stack this.game = game; this.initializeBuiltinFunctions(); } // Khởi tạo các hàm built-in initializeBuiltinFunctions() { this.functions.set('random', (min, max) => { return Math.floor(Math.random() * (max - min + 1)) + min; }); this.functions.set('choice', (choices) => { return choices[Math.floor(Math.random() * choices.length)]; }); this.functions.set('time', () => { return new Date().toISOString(); }); this.functions.set('dayOfWeek', () => { const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return days[new Date().getDay()]; }); this.functions.set('formatText', (text, ...args) => { return text.replace(/{(\d+)}/g, (match, index) => { return args[index] !== undefined ? args[index] : match; }); }); this.functions.set('lerp', (start, end, t) => { return start + (end - start) * Math.max(0, Math.min(1, t)); }); this.functions.set('distance', (x1, y1, x2, y2) => { return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); }); } // Set variable setVariable(name, value, persistent = false) { const type = this.getValueType(value); const variable = { name, value, type, persistent }; this.variables.set(name, variable); // Trigger event this.game.emit('variableChanged', { name, value, type }); } // Get variable getVariable(name) { const variable = this.variables.get(name); return variable ? variable.value : undefined; } // Check if variable exists hasVariable(name) { return this.variables.has(name); } // Delete variable deleteVariable(name) { return this.variables.delete(name); } // Get all variables getAllVariables() { return Array.from(this.variables.values()); } // Get persistent variables (for saving) getPersistentVariables() { return Array.from(this.variables.values()).filter(v => v.persistent); } // Load persistent variables (from save) loadPersistentVariables(variables) { variables.forEach(variable => { if (variable.persistent) { this.variables.set(variable.name, variable); } }); } // Evaluate expression evaluateExpression(expression) { // Simple expression parser // Supports variables, functions, basic math, and string operations try { // Replace variables let processedExpression = expression.replace(/\$(\w+)/g, (match, varName) => { const value = this.getVariable(varName); return JSON.stringify(value); }); // Replace function calls processedExpression = processedExpression.replace(/(\w+)\((.*?)\)/g, (match, funcName, args) => { const func = this.functions.get(funcName); if (func) { try { const argValues = args ? args.split(',').map((arg) => { return this.evaluateExpression(arg.trim()); }) : []; return JSON.stringify(func(...argValues)); } catch (e) { console.error(`Error calling function ${funcName}:`, e); return 'null'; } } return match; }); // Safe evaluation (limited to basic operations) return this.safeEval(processedExpression); } catch (error) { console.error('Error evaluating expression:', expression, error); return null; } } // Safe evaluation of expressions safeEval(expression) { // Whitelist of allowed operations const allowedOperators = ['+', '-', '*', '/', '%', '==', '!=', '>', '<', '>=', '<=', '&&', '||', '!']; const allowedFunctions = ['Math.floor', 'Math.ceil', 'Math.round', 'Math.abs', 'Math.min', 'Math.max']; // Remove potentially dangerous code const sanitized = expression .replace(/[^a-zA-Z0-9\s+\-*/%()=!<>&|.,'"]/g, '') .replace(/eval|function|Function|setTimeout|setInterval|require|import|export/gi, ''); try { return Function(`"use strict"; return (${sanitized})`)(); } catch (error) { console.error('Safe eval error:', error); return null; } } // Check conditions checkConditions(conditions) { if (!conditions || conditions.length === 0) return true; let result = true; let currentLogic = 'and'; for (const condition of conditions) { const variableValue = this.getVariable(condition.variable); const conditionResult = this.evaluateCondition(variableValue, condition.operator, condition.value); if (currentLogic === 'and') { result = result && conditionResult; } else { result = result || conditionResult; } currentLogic = condition.logic || 'and'; } return result; } // Evaluate single condition evaluateCondition(leftValue, operator, rightValue) { switch (operator) { case '==': return leftValue == rightValue; case '!=': return leftValue != rightValue; case '>': return Number(leftValue) > Number(rightValue); case '<': return Number(leftValue) < Number(rightValue); case '>=': return Number(leftValue) >= Number(rightValue); case '<=': return Number(leftValue) <= Number(rightValue); case 'contains': return String(leftValue).includes(String(rightValue)); case 'in': return Array.isArray(leftValue) && leftValue.includes(rightValue); default: return false; } } // Execute script commands async executeScript(commands) { for (const command of commands) { await this.executeCommand(command); } } // Execute single command async executeCommand(command) { // Check conditions if any if (command.conditions && !this.checkConditions(command.conditions)) { if (command.onFalse) { await this.executeScript(command.onFalse); } return; } switch (command.type) { case 'set': this.setVariable(command.params.name, this.evaluateExpression(command.params.value), command.params.persistent); break; case 'if': if (command.onTrue) { await this.executeScript(command.onTrue); } break; case 'jump': this.game.jumpToScene(command.params.scene, command.params.dialogue); break; case 'call': const func = this.functions.get(command.params.function); if (func) { const args = command.params.args || []; const result = func(...args); if (command.params.returnVar) { this.setVariable(command.params.returnVar, result); } } break; case 'wait': await new Promise(resolve => setTimeout(resolve, command.params.duration || 1000)); break; case 'custom': // Execute custom command through plugin system if (this.game.pluginManager) { this.game.pluginManager.executeAction(command.params.action, command.params); } break; } } // Register custom function registerFunction(name, func) { this.functions.set(name, func); } // Unregister function unregisterFunction(name) { return this.functions.delete(name); } // Get value type getValueType(value) { if (typeof value === 'string') return 'string'; if (typeof value === 'number') return 'number'; if (typeof value === 'boolean') return 'boolean'; if (Array.isArray(value)) return 'array'; if (typeof value === 'object') return 'object'; return 'string'; // default } // Process text with variables and expressions processText(text) { // Replace variables: $variableName let processed = text.replace(/\$(\w+)/g, (match, varName) => { const value = this.getVariable(varName); return value !== undefined ? String(value) : match; }); // Replace expressions: {expression} processed = processed.replace(/\{([^}]+)\}/g, (match, expression) => { const result = this.evaluateExpression(expression); return result !== null ? String(result) : match; }); return processed; } // Math utilities increment(variableName, amount = 1) { const current = Number(this.getVariable(variableName)) || 0; this.setVariable(variableName, current + amount); } decrement(variableName, amount = 1) { const current = Number(this.getVariable(variableName)) || 0; this.setVariable(variableName, current - amount); } // Array utilities pushToArray(variableName, value) { const array = this.getVariable(variableName) || []; if (Array.isArray(array)) { array.push(value); this.setVariable(variableName, array); } } popFromArray(variableName) { const array = this.getVariable(variableName); if (Array.isArray(array)) { const value = array.pop(); this.setVariable(variableName, array); return value; } return null; } // String utilities concatenateStrings(variableName, value) { const current = String(this.getVariable(variableName) || ''); this.setVariable(variableName, current + value); } // Clear all non-persistent variables clearTemporaryVariables() { const persistentVars = new Map(); this.variables.forEach((variable, name) => { if (variable.persistent) { persistentVars.set(name, variable); } }); this.variables = persistentVars; } // Export state for debugging exportState() { return { variables: Array.from(this.variables.entries()), functions: Array.from(this.functions.keys()), stack: [...this.stack] }; } }