| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>TRPG Engine</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css" /> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/hint/show-hint.min.css" /> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/lint/lint.min.css" /> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> |
| <style> |
| :root { |
| --bg-light: #f0f0f0; |
| --bg-dark: #2d2d2d; |
| --border-color: #ccc; |
| } |
| |
| |
| |
| .inventar h3 { |
| margin: 0 0 10px 0; |
| font-size: 16px; |
| color: #333; |
| } |
| |
| #text-area::-webkit-scrollbar { |
| width: 0vh; |
| } |
| |
| .time { |
| position: fixed; |
| bottom: 9vh; |
| right: 6vh; |
| color: #000000; |
| font-family: monospace; |
| } |
| |
| #control-bar { |
| transition: opacity 0.3s ease; |
| } |
| |
| #control-bar.hidden { |
| opacity: 0; |
| pointer-events: none; |
| } |
| |
| |
| .menu { |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| margin-top: 20px; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif; |
| font-size: 2vh; |
| background: var(--bg-light); |
| color: #333; |
| height: 100vh; |
| overflow: hidden; |
| } |
| |
| button { |
| cursor: pointer; |
| border: none; |
| border-radius: 1vh; |
| padding: 0.7vh 3vh; |
| font-size: 2vh; |
| background: #70b673; |
| color: #fff; |
| text-align: center; |
| } |
| |
| button:hover { |
| background: #bac16b; |
| } |
| |
| #menu { |
| background: var(--bg-dark); |
| color: #fff; |
| height: 5vh; |
| display: flex; |
| gap: 0.5vh; |
| align-items: center; |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| z-index: 100; |
| } |
| |
| #menu button { |
| background: #616161; |
| } |
| |
| #menu button:hover { |
| background: #ffa000; |
| } |
| |
| #main-container { |
| display: flex; |
| height: calc(100vh - 5vh); |
| margin-top: 5vh; |
| overflow: hidden; |
| } |
| |
| .loc { |
| width: 100%; |
| border-radius: 1vh; |
| } |
| |
| |
| .loc2 { |
| width: 100%; |
| border-style: solid; |
| padding: 2vh; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| } |
| |
| .menu { |
| display: flex; |
| flex-direction: row; |
| gap: 0.5vh; |
| } |
| |
| .menu button { |
| width: 100%; |
| } |
| |
| .content-area { |
| display: flex; |
| flex: 1; |
| height: -webkit-fill-available; |
| overflow: hidden; |
| } |
| |
| #editor-container { |
| width: -webkit-fill-available; |
| } |
| |
| #sidebar { |
| width: 30vh; |
| background: #767676; |
| color: #fff; |
| padding: 1vh; |
| overflow-y: auto; |
| } |
| |
| #sidebar h3 { |
| margin-bottom: 0.5vh; |
| font-size: 1.5vh; |
| text-align: center; |
| } |
| |
| #tree { |
| list-style: none; |
| padding-left: 0; |
| } |
| |
| #tree > li { |
| padding: 0.5vh; |
| margin-bottom: 0.3vh; |
| background: #484848; |
| border-radius: 0.5vh; |
| padding-left: 2vh; |
| cursor: pointer; |
| } |
| |
| #tree li .toggle { |
| font-size: 0.9em; |
| margin-right: 0.4em; |
| cursor: pointer; |
| } |
| |
| #tree li ul { |
| list-style: none; |
| margin-left: 1.2em; |
| padding-left: 0.5em; |
| border-left: 1px dashed var(--border-color); |
| display: none; |
| margin-top: 0.3em; |
| } |
| |
| #tree li ul li { |
| padding: 0.3em; |
| margin-bottom: 0.2em; |
| border: 1px solid var(--border-color); |
| border-radius: 4px; |
| cursor: pointer; |
| background: #424242; |
| } |
| |
| #tree li ul li.active { |
| background: #ffa000; |
| } |
| |
| #sidebar-buttons { |
| position: absolute; |
| bottom: 1vh; |
| width: 28vh; |
| margin-top: 1vh; |
| display: flex; |
| gap: 1.5vh; |
| justify-content: space-around; |
| } |
| |
| #sidebar-buttons button { |
| flex: 1; |
| font-size: 2vh; |
| } |
| |
| #editor-wrapper { |
| flex: 1; |
| position: relative; |
| overflow: hidden; |
| height: 100%; |
| } |
| |
| |
| .CodeMirror-gutter { |
| background: #5d5e5f; |
| } |
| |
| .CodeMirror-scroll { |
| background: #272727; |
| color: #ffebd4; |
| } |
| |
| .CodeMirror-cursor { |
| border-left: 0.3vh solid #ffffff; |
| } |
| |
| .CodeMirror::-webkit-scrollbar { |
| width: 0vh; |
| } |
| |
| .CodeMirror { |
| height: 100% !important; |
| font-family: "JetBrains Mono", "Fira Code", monospace; |
| font-size: 2.5vh; |
| background: #fff; |
| overflow-y: auto; |
| } |
| |
| #overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0,0,0,0.6); |
| backdrop-filter: blur(4px); |
| z-index: 1000; |
| display: none; |
| justify-content: center; |
| align-items: center; |
| } |
| |
| #game-container { |
| background: #c7c3c1; |
| width: 100%; |
| height: 100%; |
| display: none; |
| flex-direction: column; |
| overflow: hidden; |
| z-index: 1001; |
| } |
| |
| .game-layout { |
| display: flex; |
| flex: 1; |
| } |
| |
| #text-area { |
| flex: 1; |
| height: 100vh; |
| padding-top: 3vh; |
| padding-bottom: 2vh; |
| padding-left: 30vh; |
| padding-right: 30vh; |
| margin-right: 30vh; |
| overflow-y: auto; |
| background: #c7c3c1; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| } |
| |
| .nav-link { |
| color: red; |
| cursor: pointer; |
| } |
| |
| .inventar { |
| position: fixed; |
| right: 0; |
| top: 0; |
| width: 30vh; |
| height: 94vh; |
| background: #cbcbcb; |
| padding: 2vh; |
| overflow-y: auto; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| text-align: center; |
| font-size: 2vh; |
| font-weight: bold; |
| } |
| |
| #control-bar { |
| position: fixed; |
| bottom: 0; |
| right: 0; |
| width: 30vh; |
| display: flex; |
| height: 6vh; |
| gap: 1.2vh; |
| } |
| |
| #control-bar button { |
| flex: 1; |
| background: #3b3b3b; |
| border: solid #cabdb2; |
| font-size: 3vh; |
| padding: 0; |
| margin: 0.5vh; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="menu"> |
| <button id="btnNew"><i class="fas fa-file"></i> Новый</button> |
| <button id="btnOpen"><i class="fas fa-folder-open"></i> Открыть</button> |
| <button id="btnSave"><i class="fas fa-save"></i> Сохранить</button> |
| <button id="btnPlay"><i class="fas fa-play"></i> Играть</button> |
| <button id="btnExport"><i class="fas fa-download"></i> Экспорт в Web</button> |
| <button id="btnExportExe">Экспорт в EXE</button> |
| <a href="docs.html" target="_blank" id="btnGuide" style="background: #575757;padding: 0.5vh;border-radius: 1vh;padding-left: 2vh;padding-right: 2vh;color: white;font-size: 2vh;"><i class="fas fa-question-circle"></i> Руководство</a> |
| <input type="file" id="fileInput" accept=".txt" style="display:none" /> |
| </div> |
| <div id="main-container"> |
| <div id="editor-container"> |
| <div class="content-area"> |
| <div id="sidebar"> |
| <h3>Локации</h3> |
| <ul id="tree"></ul> |
| </div> |
| <div id="editor-wrapper"> |
| <textarea id="editor"></textarea> |
| </div> |
| </div> |
| </div> |
| <div id="overlay"> |
| <div id="game-container"> |
| <div class="game-layout"> |
| <div id="text-area"><div class="game-content"></div></div> |
| </div> |
| <div id="control-bar"> |
| <button id="save-game"><i class="fas fa-save"></i></button> |
| <button id="load-game"><i class="fas fa-folder-open"></i></button> |
| <button id="stop-game"><i class="fas fa-stop"></i></button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| let scriptContent = ""; |
| |
| |
| class Game { |
| constructor() { |
| this.content = ""; |
| this.state = {}; |
| this.locations = {}; |
| this.currentLocation = 'начало'; |
| this.currentCase = '0'; |
| this.audio = null; |
| this.cacheElements(); |
| this.initEventListeners(); |
| } |
| |
| |
| cacheElements() { |
| this.textArea = document.querySelector('#game-container .game-content'); |
| this.loadInput = document.getElementById('load-input'); |
| |
| if (!this.loadInput) { |
| this.loadInput = document.createElement('input'); |
| this.loadInput.type = 'file'; |
| this.loadInput.id = 'load-input'; |
| this.loadInput.style.display = 'none'; |
| this.loadInput.accept = '.json'; |
| document.body.appendChild(this.loadInput); |
| } |
| } |
| |
| |
| setBackground(url) { |
| document.getElementById('game-container').style.backgroundImage = `url('${url}')`; |
| } |
| |
| |
| playSound(soundPath, loop) { |
| if (!this.audio) { this.audio = new Audio(); } |
| this.audio.src = soundPath; |
| this.audio.loop = loop; |
| this.audio.play(); |
| } |
| |
| |
| loadScriptFromText(scriptText) { |
| this.content = scriptText; |
| this.locations = this.parseLocations(this.content); |
| if (this.locations["переменные"]) { |
| let varLines = (this.locations["переменные"].content || "").split('\n'); |
| varLines.forEach(line => { |
| line = line.trim(); |
| if (line.startsWith("img") || line.startsWith("play") || line.startsWith("rn")) { |
| this.processCommand(line); |
| } else if (line.startsWith("s ")) { |
| this.setVariable(line); |
| } |
| }); |
| } |
| this.currentLocation = "начало"; |
| this.currentCase = "0"; |
| this.displayContent(); |
| this.updateTextAreaStyle(); |
| this.updateControlBarVisibility(); |
| } |
| |
| |
| updateControlBarVisibility() { |
| const controlBar = document.getElementById('control-bar'); |
| if (!controlBar) return; |
| |
| if (this.currentLocation === 'начало') { |
| controlBar.style.display = 'none'; |
| } else { |
| controlBar.style.display = 'flex'; |
| } |
| } |
| |
| |
| parseLocations(text) { |
| const lines = text.split('\n'); |
| let locs = {}, currentLoc = null, currentCase = null; |
| lines.forEach(line => { |
| let trimmed = line.trim(); |
| if (trimmed.startsWith("loc ")) { |
| let locName = trimmed.substring(4).trim().toLowerCase(); |
| currentLoc = locName; |
| locs[currentLoc] = { cases: {} }; |
| currentCase = null; |
| } else if (trimmed.startsWith("case ")) { |
| if (!currentLoc) return; |
| let caseName = trimmed.substring(5).trim(); |
| currentCase = caseName; |
| locs[currentLoc].cases[currentCase] = ""; |
| } else { |
| if (!currentLoc) return; |
| if (currentLoc === "переменные" && currentCase === null) { |
| locs[currentLoc].content = (locs[currentLoc].content || "") + line + "\n"; |
| } else if (currentCase) { |
| locs[currentLoc].cases[currentCase] += line + "\n"; |
| } |
| } |
| }); |
| return locs; |
| } |
| |
| |
| stopGame() { |
| this.content = ""; |
| this.state = {}; |
| document.getElementById('game-container').style.display = 'none'; |
| document.getElementById('overlay').style.display = 'none'; |
| |
| if (this.audio) { |
| this.audio.pause(); |
| this.audio.currentTime = 0; |
| } |
| } |
| |
| |
| displayContent() { |
| if (!this.textArea) return; |
| |
| let currentText = ""; |
| if (this.locations[this.currentLocation] && this.locations[this.currentLocation].cases[this.currentCase]) { |
| currentText = this.locations[this.currentLocation].cases[this.currentCase]; |
| } |
| |
| currentText = this.processRandomCommands(currentText); |
| let processed = this.processContentText(currentText); |
| |
| if (this.currentLocation !== "начало" && this.locations["инвентарь"] && this.locations["инвентарь"].cases["0"]) { |
| const invProcessed = this.processContentText(this.locations["инвентарь"].cases["0"]); |
| processed += `<br><div class="inventar"><h3>Инвентарь</h3>${invProcessed}</div>`; |
| } |
| |
| this.textArea.innerHTML = this.renderText(processed); |
| } |
| |
| |
| updateTextAreaStyle() { |
| const textArea = document.getElementById('text-area'); |
| if (!textArea) return; |
| |
| if (this.currentLocation === 'начало') { |
| textArea.style.paddingRight = '30vh'; |
| textArea.style.marginRight = '0vh'; |
| } else { |
| textArea.style.paddingRight = '30vh'; |
| textArea.style.marginRight = '30vh'; |
| } |
| } |
| |
| |
| processRandomCommands(text) { |
| let lines = text.split('\n'); |
| let resultLines = []; |
| let rnBlock = []; |
| for (let line of lines) { |
| let trimmed = line.trim(); |
| if (trimmed.startsWith("rn ")) { |
| rnBlock.push(trimmed.substring(3).trim()); |
| } else { |
| if (rnBlock.length > 0) { |
| let selected = rnBlock[Math.floor(Math.random() * rnBlock.length)]; |
| resultLines.push(selected); |
| rnBlock = []; |
| } |
| resultLines.push(line); |
| } |
| } |
| if (rnBlock.length > 0) { |
| let selected = rnBlock[Math.floor(Math.random() * rnBlock.length)]; |
| resultLines.push(selected); |
| } |
| return resultLines.join('\n'); |
| } |
| |
| |
| processContentText(text) { |
| text = text.replace(/img\s+"([^"]+)"(?:\s+class\s*=\s*"([^"]+)")?/gi, |
| (match, path, cls) => cls ? `<img src="${path}" class="${cls}">` : `<img src="${path}">`); |
| |
| text = text.replace(/<([a-z][a-z0-9]*)([^>]*)>([^<]*)<\/\1>/gi, (match, tag, attrs, content) => { |
| const processedContent = this.processInlineConditions(content); |
| return `<${tag}${attrs}>${processedContent}</${tag}>`; |
| }); |
| |
| const lines = text.split('\n'); |
| const output = []; |
| let i = 0; |
| |
| const standaloneIfElseRegex = /^if\s+(.+?)\s+\(([^)]+)\)\s+else\s+\(([^)]+)\)\s*\/\/$/i; |
| const standaloneIfRegex = /^if\s+(.+?)\s+\(([^)]+)\)\s*\/\/$/i; |
| |
| while (i < lines.length) { |
| const line = lines[i].trim(); |
| |
| if (standaloneIfElseRegex.test(line)) { |
| const match = line.match(standaloneIfElseRegex); |
| const condition = match[1]; |
| const trueText = match[2]; |
| const falseText = match[3]; |
| const chosenText = this.evaluateCondition(condition) ? trueText : falseText; |
| output.push(chosenText); |
| i++; |
| } |
| else if (standaloneIfRegex.test(line)) { |
| const match = line.match(standaloneIfRegex); |
| const condition = match[1]; |
| const trueText = match[2]; |
| if (this.evaluateCondition(condition)) { |
| output.push(trueText); |
| } |
| i++; |
| } |
| else if (/^if\s+/i.test(line) && !line.includes('else')) { |
| let condition = line.slice(3).trim(); |
| let block = []; |
| i++; |
| while (i < lines.length && !/^\s*\/\//.test(lines[i].trim())) { |
| block.push(lines[i]); |
| i++; |
| } |
| if (this.evaluateCondition(condition)) { |
| const processedBlock = this.processContentText(block.join('\n')); |
| output.push(processedBlock); |
| } |
| i++; |
| } |
| else if (/^s\s+/i.test(line)) { |
| this.setVariable(line); |
| i++; |
| } |
| else if (/^(img|play|rn)\s+/i.test(line)) { |
| const result = this.processCommand(line); |
| if (result) output.push(result); |
| i++; |
| } |
| else if (/^\(m\s+|^\(меню\s+/i.test(line)) { |
| output.push(this.processMenuCommand(line)); |
| i++; |
| } |
| else if (/^\((loc|case|l|c)\s+/i.test(line)) { |
| output.push(this.processNavigationLink(line)); |
| i++; |
| } |
| else { |
| const processedLine = this.processTextWithLinks(lines[i]); |
| output.push(processedLine); |
| i++; |
| } |
| } |
| return output.join('\n'); |
| } |
| |
| |
| processInlineConditions(text) { |
| text = text.replace(/if\s+(.+?)\s+\(([^)]+)\)\s+else\s+\(([^)]+)\)\s*\/\//gi, |
| (match, condition, trueText, falseText) => { |
| return this.evaluateCondition(condition) ? trueText : falseText; |
| }); |
| |
| text = text.replace(/if\s+(.+?)\s+\(([^)]+)\)\s*\/\//gi, |
| (match, condition, trueText) => { |
| return this.evaluateCondition(condition) ? trueText : ''; |
| }); |
| |
| return text; |
| } |
| |
| |
| processTextWithLinks(text) { |
| text = text.replace(/\(item if\s+([^\)]+)\s+\(([^\)]+)\s+\(([^\|]+)\|\s*([^\)]+)\)\)\)/gi, |
| (match, condition, text, item, action) => { |
| if (this.evaluateCondition(condition.trim())) { |
| return `<span>${text}</span> <a class="nav-link" data-action="item" data-command="${action.trim()}">${item}</a>`; |
| } |
| return ''; |
| }); |
| |
| text = text.replace(/\((loc|case|l|c)\s+([^\-]+\s*-\s*[^\)]+)\)/gi, |
| (match, type, content) => { |
| const shortType = type.length === 1 ? (type === 'l' ? 'loc' : 'case') : type; |
| const [display, target] = content.split('-').map(s => s.trim()); |
| return `<a class="nav-link" data-action="navigate" data-type="${shortType}" data-target="${target.toLowerCase()}">${display}</a>`; |
| }); |
| |
| text = text.replace(/if\s+(.+?)\s+\(([^)]+)\)\s+else\s+\(([^)]+)\)\s*\/\//gi, |
| (match, condition, trueText, falseText) => { |
| return this.evaluateCondition(condition) ? trueText : falseText; |
| }); |
| |
| return text; |
| } |
| |
| |
| processCommand(cmd) { |
| cmd = cmd.trim(); |
| |
| if (cmd.startsWith("rn ")) { |
| return ""; |
| } |
| |
| if (cmd.startsWith("img ")) { |
| const newFormatRegex = /^img\s+"([^"]+)"(?:\s+class\s*=\s*"([^"]+)")?/i; |
| const newMatch = cmd.match(newFormatRegex); |
| if (newMatch) { |
| const imgPath = newMatch[1]; |
| const imgClass = newMatch[2] ? `class="${newMatch[2]}"` : ''; |
| return `<img src="${imgPath}" ${imgClass}>`; |
| } |
| |
| const oldFormatRegex = /^img\s+"([^"]+)"\s+class\s+"([^"]+)"$/i; |
| const oldMatch = cmd.match(oldFormatRegex); |
| if (oldMatch && oldMatch[2] === "loc") { |
| this.setBackground(oldMatch[1]); |
| } |
| return ''; |
| } |
| |
| if (cmd.startsWith("play ")) { |
| const playRegex = /^play\s+"([^"]+)"\s*(loop)?$/i; |
| const match = cmd.match(playRegex); |
| if (match) { |
| this.playSound(match[1], !!match[2]); |
| } |
| return ''; |
| } |
| |
| return cmd; |
| } |
| |
| |
| setVariable(line) { |
| let assignment = line.substring(2).trim(); |
| let opMatch = assignment.match(/^(\w+)\s*\+=\s*(.+)$/); |
| if (opMatch) { |
| let varName = opMatch[1].trim(); |
| let valueStr = opMatch[2].trim(); |
| let value; |
| if (valueStr.startsWith('"') && valueStr.endsWith('"')) { |
| value = valueStr.substring(1, valueStr.length - 1); |
| } else if (valueStr.toLowerCase() === "true") { |
| value = true; |
| } else if (valueStr.toLowerCase() === "false") { |
| value = false; |
| } else if (!isNaN(valueStr)) { |
| value = parseFloat(valueStr); |
| } else { |
| value = valueStr; |
| } |
| if (this.state.hasOwnProperty(varName) && typeof this.state[varName] === "number" && typeof value === "number") { |
| this.state[varName] += value; |
| } else { |
| this.state[varName] = value; |
| } |
| return; |
| } |
| let parts = assignment.split('='); |
| if (parts.length !== 2) return; |
| let varName = parts[0].trim(); |
| let valueStr = parts[1].trim(); |
| let value; |
| if (valueStr.startsWith('"') && valueStr.endsWith('"')) { |
| value = valueStr.substring(1, valueStr.length - 1); |
| } else if (valueStr.toLowerCase() === "true") { |
| value = true; |
| } else if (valueStr.toLowerCase() === "false") { |
| value = false; |
| } else if (!isNaN(valueStr)) { |
| value = parseFloat(valueStr); |
| } else { |
| value = valueStr; |
| } |
| this.state[varName] = value; |
| } |
| |
| |
| processMenuCommand(cmd) { |
| const regex = /\((l|c)\s+([^-]+?)\s*-\s*([^)]+?)\)/gi; |
| let match; |
| const buttons = []; |
| while ((match = regex.exec(cmd)) !== null) { |
| const type = match[1].trim().toLowerCase() === 'l' ? 'loc' : 'case'; |
| const display = match[2].trim(); |
| const target = match[3].trim().toLowerCase(); |
| buttons.push(`<button data-action="navigate" data-type="${type}" data-target="${target}">${display}</button>`); |
| } |
| return `<div class="menu">${buttons.join(' ')}</div>`; |
| } |
| |
| |
| processNavigationLink(cmd) { |
| const regex = /\((loc|case|l|c)\s+([^-]+?)\s*-\s*([^)]+?)\)/i; |
| const match = cmd.match(regex); |
| if (match) { |
| const type = match[1].length === 1 ? (match[1] === 'l' ? 'loc' : 'case') : match[1]; |
| const display = match[2].trim(); |
| const target = match[3].trim().toLowerCase(); |
| return `<a class="nav-link" data-action="navigate" data-type="${type}" data-target="${target}">${display}</a>`; |
| } |
| return cmd; |
| } |
| |
| |
| evaluateCondition(cond) { |
| let butRegex = /^(\w+)\s*(>=|>|<=|<|==)\s*([\d.]+)\s+but\s+(<=|<|>=|>|==)\s*([\d.]+)$/i; |
| let match = cond.match(butRegex); |
| if (match) { |
| let varName = match[1]; |
| let op1 = match[2]; |
| let val1 = match[3]; |
| let op2 = match[4]; |
| let val2 = match[5]; |
| cond = `${varName} ${op1} ${val1} && ${varName} ${op2} ${val2}`; |
| } |
| |
| Object.keys(this.state).forEach(key => { |
| const value = this.state[key]; |
| if (typeof value === 'string') { |
| cond = cond.replace(new RegExp(`\\b${key}\\b`, 'g'), `"${value}"`); |
| } else if (typeof value === 'boolean') { |
| cond = cond.replace(new RegExp(`\\b${key}\\b`, 'g'), value ? 'true' : 'false'); |
| } else { |
| cond = cond.replace(new RegExp(`\\b${key}\\b`, 'g'), value); |
| } |
| }); |
| |
| try { |
| return Function("return " + cond)(); |
| } |
| catch (e) { |
| console.error('Ошибка в условии:', cond, e); |
| return false; |
| } |
| } |
| |
| |
| renderText(text) { |
| let processed = text.replace(/\{(\w+)\}/g, (match, p1) => |
| this.state[p1] !== undefined ? this.state[p1] : match |
| ); |
| |
| return processed |
| .split('\n') |
| .map(line => line.trim()) |
| .filter(line => line !== '') |
| .join('<br>'); |
| } |
| |
| |
| incrementTime(minutes) { |
| let min = this.state.min || 0; |
| let hour = this.state.hour || 0; |
| let day = this.state.day || 1; |
| let month = this.state.month || 1; |
| let year = this.state.year || 1950; |
| |
| min += minutes; |
| while (min >= 60) { |
| min -= 60; |
| hour += 1; |
| } |
| while (hour >= 24) { |
| hour -= 24; |
| day += 1; |
| } |
| while (day > 30) { |
| day -= 30; |
| month += 1; |
| } |
| while (month > 12) { |
| month -= 12; |
| year += 1; |
| } |
| |
| this.state.min = min; |
| this.state.hour = hour; |
| this.state.day = day; |
| this.state.month = month; |
| this.state.year = year; |
| } |
| |
| |
| navigateTo(target, type) { |
| const oldLocation = this.currentLocation; |
| |
| if (target.includes('/')) { |
| const [loc, caseName] = target.split('/'); |
| this.currentLocation = loc.toLowerCase(); |
| this.currentCase = caseName.toLowerCase(); |
| } else if (type === 'loc') { |
| this.currentLocation = target.toLowerCase(); |
| this.currentCase = "0"; |
| } else if (type === 'case') { |
| this.currentCase = target.toLowerCase(); |
| } else { |
| if (this.locations[target.toLowerCase()]) { |
| this.currentLocation = target.toLowerCase(); |
| this.currentCase = "0"; |
| } else if (this.locations[this.currentLocation]?.cases[target]) { |
| this.currentCase = target; |
| } |
| } |
| |
| if (this.currentLocation !== oldLocation) { |
| this.incrementTime(this.state.clik || 10); |
| } |
| |
| this.displayContent(); |
| this.updateTextAreaStyle(); |
| this.updateControlBarVisibility(); |
| } |
| |
| |
| saveGame() { |
| const data = { |
| content: this.content, |
| state: this.state, |
| currentLocation: this.currentLocation, |
| currentCase: this.currentCase |
| }; |
| const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); |
| const url = URL.createObjectURL(blob); |
| let a = document.createElement('a'); |
| a.href = url; |
| a.download = 'game_save.json'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| alert('Игра сохранена!'); |
| } |
| |
| |
| loadGame() { |
| if (!this.loadInput) { |
| this.cacheElements(); |
| } |
| |
| if (this.loadInput) { |
| this.loadInput.value = ''; |
| this.loadInput.click(); |
| } else { |
| console.error('Элемент для загрузки не найден!'); |
| const input = document.createElement('input'); |
| input.type = 'file'; |
| input.accept = '.json'; |
| input.onchange = (e) => this.loadGameFromFile(e.target.files[0]); |
| input.click(); |
| } |
| } |
| |
| |
| loadGameFromFile(file) { |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| try { |
| const data = JSON.parse(e.target.result); |
| this.content = data.content; |
| this.state = data.state || {}; |
| this.currentLocation = data.currentLocation || 'начало'; |
| this.currentCase = data.currentCase || '0'; |
| this.locations = this.parseLocations(this.content); |
| this.displayContent(); |
| alert('Игра загружена!'); |
| } catch (err) { |
| alert('Ошибка при загрузке: ' + err.message); |
| } |
| }; |
| reader.onerror = () => alert('Ошибка чтения файла'); |
| reader.readAsText(file); |
| } |
| |
| |
| initEventListeners() { |
| document.getElementById('save-game').addEventListener('click', () => this.saveGame()); |
| document.getElementById('load-game').addEventListener('click', () => this.loadGame()); |
| document.getElementById('stop-game').addEventListener('click', () => this.stopGame()); |
| |
| if (this.loadInput) { |
| this.loadInput.addEventListener('change', (e) => { |
| if (e.target.files.length > 0) { |
| this.loadGameFromFile(e.target.files[0]); |
| } |
| }); |
| } |
| |
| if (this.textArea) { |
| this.textArea.addEventListener('click', (e) => { |
| const el = e.target; |
| if (el.dataset.action) { |
| e.preventDefault(); |
| const action = el.dataset.action; |
| if (action === 'navigate') this.navigateTo(el.dataset.target, el.dataset.type); |
| else if (action === 'item') { |
| this.setVariable(el.dataset.command); |
| this.displayContent(); |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| |
| const editor = CodeMirror.fromTextArea(document.getElementById("editor"), { |
| lineNumbers: true, |
| mode: "custom", |
| indentUnit: 4, |
| tabSize: 4, |
| historyEventDelay: 0, |
| indentWithTabs: false, |
| extraKeys: { |
| "Ctrl-Space": "autocomplete", |
| "Tab": (cm) => { cm.replaceSelection(" ", "end"); }, |
| "Ctrl-Z": (cm) => { cm.undo(); }, |
| "Ctrl-S": (cm) => { saveProjectToFile(cm.getValue()); } |
| }, |
| lineWrapping: true, |
| lint: true |
| }); |
| |
| |
| function exportToWeb() { |
| |
| const scriptContent = codeMirrorEditor.getValue(); |
| |
| if (!scriptContent || scriptContent.trim() === '') { |
| alert('Редактор пуст! Нечего экспортировать.'); |
| return; |
| } |
| |
| const htmlContent = ` |
| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Моя Игра</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> |
| <style> |
| #text-area::-webkit-scrollbar { |
| width: 0; |
| } |
| |
| body { |
| margin: 0; |
| } |
| |
| #game-container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| width: 100%; |
| height: 100vh; |
| background-size: cover; |
| background-position: center; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .loc { |
| width: 100%; |
| border-radius: 1vh; |
| } |
| |
| .menu { |
| display: flex; |
| flex-direction: row; |
| gap: 10px; |
| margin-top: 20px; |
| } |
| |
| .menu button { |
| width: 100%; |
| } |
| |
| .loc2 { |
| width: 100%; |
| border-style: solid; |
| padding: 2vh; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| } |
| |
| .game-layout { |
| display: flex; |
| width: 100%; |
| height: calc(100vh - 6vh); /* Учитываем высоту control-bar */ |
| } |
| |
| .time { |
| position: fixed; |
| bottom: 9vh; |
| right: 6vh; |
| color: #000000; |
| font-family: monospace; |
| } |
| |
| button { |
| cursor: pointer; |
| border: none; |
| border-radius: 1vh; |
| padding: 0.7vh 3vh; |
| font-size: 2vh; |
| background: #70b673; |
| color: #fff; |
| text-align: center; |
| } |
| |
| button:hover { |
| background: #bac16b; |
| } |
| |
| #text-area { |
| flex: 1; |
| height: calc(100vh - 0vh); |
| padding: 3vh 30vh 2vh; |
| overflow-y: auto; |
| background: #c7c3c1; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| box-sizing: border-box; |
| } |
| |
| .nav-link { |
| color: red; |
| cursor: pointer; |
| } |
| |
| .inventar { |
| position: fixed; |
| right: 0; |
| top: 0; |
| width: 30vh; |
| height: calc(100vh - 6vh); /* Учитываем высоту control-bar */ |
| background: #cbcbcb; |
| padding: 2vh; |
| overflow-y: auto; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| text-align: center; |
| font-size: 2vh; |
| font-weight: bold; |
| box-sizing: border-box; |
| } |
| |
| #control-bar { |
| position: fixed; |
| bottom: 0; |
| right: 0; |
| width: 30vh; |
| display: flex; |
| height: 6vh; |
| gap: 1.2vh; |
| background: #3b3b3b; |
| padding: 0.5vh; |
| box-sizing: border-box; |
| } |
| |
| #control-bar button { |
| flex: 1; |
| background: #3b3b3b; |
| border: solid #cabdb2; |
| font-size: 3vh; |
| padding: 0; |
| margin: 0; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="game-container"> |
| <div class="game-layout"> |
| <div id="text-area"><div class="game-content"></div></div> |
| </div> |
| <div id="control-bar"> |
| <button id="save-game"><i class="fas fa-save"></i></button> |
| <button id="load-game"><i class="fas fa-folder-open"></i></button> |
| </div> |
| </div> |
| <script> |
| ${Game.toString()} |
| |
| // Адаптация методов для экспортированной игры |
| Game.prototype.saveGame = function() { |
| const data = { |
| content: this.content, |
| state: this.state, |
| currentLocation: this.currentLocation, |
| currentCase: this.currentCase |
| }; |
| localStorage.setItem('gameSave', JSON.stringify(data)); |
| alert('Игра сохранена!'); |
| }; |
| |
| Game.prototype.loadGame = function() { |
| const savedData = localStorage.getItem('gameSave'); |
| if (savedData) { |
| const data = JSON.parse(savedData); |
| this.content = data.content; |
| this.state = data.state || {}; |
| this.currentLocation = data.currentLocation || 'начало'; |
| this.currentCase = data.currentCase || '0'; |
| this.locations = this.parseLocations(this.content); |
| this.displayContent(); |
| alert('Игра загружена!'); |
| } else { |
| alert('Нет сохраненной игры!'); |
| } |
| }; |
| |
| Game.prototype.initEventListeners = function() { |
| document.getElementById('save-game').addEventListener('click', () => this.saveGame()); |
| document.getElementById('load-game').addEventListener('click', () => this.loadGame()); |
| |
| if (this.textArea) { |
| this.textArea.addEventListener('click', (e) => { |
| const el = e.target; |
| if (el.dataset.action) { |
| e.preventDefault(); |
| const action = el.dataset.action; |
| if (action === 'navigate') this.navigateTo(el.dataset.target, el.dataset.type); |
| else if (action === 'item') { |
| this.setVariable(el.dataset.command); |
| this.displayContent(); |
| } |
| } |
| }); |
| } |
| }; |
| |
| // Инициализация игры |
| document.addEventListener('DOMContentLoaded', function() { |
| const game = new Game(); |
| game.loadScriptFromText(${JSON.stringify(scriptContent)}); |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: absolute; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">This website has been generated by <a href="https://enzostvs-deepsite.hf.space" style="color: #fff;" target="_blank" >DeepSite</a> <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;"></p></body> |
| </html> |
| `; |
|
|
| // Создание и скачивание файла |
| const blob = new Blob([htmlContent], { type: 'text/html' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'game.html'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| } |
|
|
| // Общая функция для генерации HTML-контента |
| function generateGameHtml(scriptContent) { |
| return ` |
| <!DOCTYPE html> |
| <html lang="ru"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Моя Игра</title> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" /> |
| <style> |
| #text-area::-webkit-scrollbar { |
| width: 0; |
| } |
| |
| body { |
| margin: 0; |
| } |
| |
| #game-container { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| width: 100%; |
| height: 100vh; |
| background-size: cover; |
| background-position: center; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .loc { |
| width: 100%; |
| border-radius: 1vh; |
| } |
| |
| .menu { |
| display: flex; |
| flex-direction: row; |
| gap: 10px; |
| margin-top: 20px; |
| } |
| |
| .menu button { |
| width: 100%; |
| } |
| |
| .loc2 { |
| width: 100%; |
| border-style: solid; |
| padding: 2vh; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| } |
| |
| .game-layout { |
| display: flex; |
| width: 100%; |
| height: calc(100vh - 6vh); |
| } |
| |
| .time { |
| position: fixed; |
| bottom: 9vh; |
| right: 6vh; |
| color: #000000; |
| font-family: monospace; |
| } |
| |
| button { |
| cursor: pointer; |
| border: none; |
| border-radius: 1vh; |
| padding: 0.7vh 3vh; |
| font-size: 2vh; |
| background: #70b673; |
| color: #fff; |
| text-align: center; |
| } |
| |
| button:hover { |
| background: #bac16b; |
| } |
| |
| #text-area { |
| flex: 1; |
| height: calc(100vh - 0vh); |
| padding: 3vh 30vh 2vh; |
| overflow-y: auto; |
| background: #c7c3c1; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| box-sizing: border-box; |
| } |
| |
| .nav-link { |
| color: red; |
| cursor: pointer; |
| } |
| |
| .inventar { |
| position: fixed; |
| right: 0; |
| top: 0; |
| width: 30vh; |
| height: calc(100vh - 6vh); |
| background: #cbcbcb; |
| padding: 2vh; |
| overflow-y: auto; |
| border-style: solid; |
| border-image-width: 2vh; |
| border-image-source: url(img/hud/uzor.png); |
| border-image-slice: 30; |
| border-image-repeat: round; |
| text-align: center; |
| font-size: 2vh; |
| font-weight: bold; |
| box-sizing: border-box; |
| } |
| |
| #control-bar { |
| position: fixed; |
| bottom: 0; |
| right: 0; |
| width: 30vh; |
| display: flex; |
| height: 6vh; |
| gap: 1.2vh; |
| background: #3b3b3b; |
| padding: 0.5vh; |
| box-sizing: border-box; |
| } |
| |
| #control-bar button { |
| flex: 1; |
| background: #3b3b3b; |
| border: solid #cabdb2; |
| font-size: 3vh; |
| padding: 0; |
| margin: 0; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="game-container"> |
| <div class="game-layout"> |
| <div id="text-area"><div class="game-content"></div></div> |
| </div> |
| <div id="control-bar"> |
| <button id="save-game"><i class="fas fa-save"></i></button> |
| <button id="load-game"><i class="fas fa-folder-open"></i></button> |
| </div> |
| </div> |
| <script> |
| ${Game.toString()} |
| |
| Game.prototype.saveGame = function() { |
| const data = { |
| content: this.content, |
| state: this.state, |
| currentLocation: this.currentLocation, |
| currentCase: this.currentCase |
| }; |
| localStorage.setItem('gameSave', JSON.stringify(data)); |
| alert('Игра сохранена!'); |
| }; |
| |
| Game.prototype.loadGame = function() { |
| const savedData = localStorage.getItem('gameSave'); |
| if (savedData) { |
| const data = JSON.parse(savedData); |
| this.content = data.content; |
| this.state = data.state || {}; |
| this.currentLocation = data.currentLocation || 'начало'; |
| this.currentCase = data.currentCase || '0'; |
| this.locations = this.parseLocations(this.content); |
| this.displayContent(); |
| alert('Игра загружена!'); |
| } else { |
| alert('Нет сохраненной игры!'); |
| } |
| }; |
| |
| Game.prototype.initEventListeners = function() { |
| document.getElementById('save-game').addEventListener('click', () => this.saveGame()); |
| document.getElementById('load-game').addEventListener('click', () => this.loadGame()); |
| |
| if (this.textArea) { |
| this.textArea.addEventListener('click', (e) => { |
| const el = e.target; |
| if (el.dataset.action) { |
| e.preventDefault(); |
| const action = el.dataset.action; |
| if (action === 'navigate') this.navigateTo(el.dataset.target, el.dataset.type); |
| else if (action === 'item') { |
| this.setVariable(el.dataset.command); |
| this.displayContent(); |
| } |
| } |
| }); |
| } |
| }; |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| const game = new Game(); |
| game.loadScriptFromText(${JSON.stringify(scriptContent)}); |
| }); |
| </script> |
| </body> |
| </html> |
| `; |
| } |
|
|
| // Экспорт в EXE (Electron) |
| function exportToExe() { |
| const scriptContent = codeMirrorEditor.getValue(); |
| if (!scriptContent.trim()) { |
| alert('Редактор пуст! Нечего экспортировать.'); |
| return; |
| } |
|
|
| const htmlContent = generateGameHtml(scriptContent); |
| |
| // Содержимое BAT-файла с русским названием и кодировкой CP866 |
| const batContent = `@echo off |
| REM build.bat - Скрипт для автоматической сборки проекта |
| setlocal enabledelayedexpansion |
|
|
| ECHO. |
| ECHO ============================================== |
| ECHO === Запуск установки зависимостей и сборки === |
| ECHO ============================================== |
| ECHO. |
|
|
| REM 1. Установка зависимостей |
| ECHO [1/3] Устанавливаем зависимости npm... |
| CALL npm install |
| IF %ERRORLEVEL% NEQ 0 ( |
| ECHO ОШИБКА! Не удалось установить зависимости |
| TIMEOUT /T 5 |
| EXIT /B 1 |
| ) |
|
|
| REM 2. Запуск сборки проекта |
| ECHO [2/3] Запускаем сборку проекта... |
| CALL npm run build |
| IF %ERRORLEVEL% NEQ 0 ( |
| ECHO ОШИБКА! Сборка не удалась |
| TIMEOUT /T 5 |
| EXIT /B 1 |
| ) |
|
|
| REM 3. Завершение работы |
| ECHO [3/3] Сборка успешно завершена! |
| ECHO. |
| ECHO Файлы проекта доступны в папке /dist |
| ECHO. |
|
|
| TIMEOUT /T 10 |
| EXIT /B 0 |
| `; |
|
|
| const readmeTxt = `Молодец что ты тут, так будет проще: |
| 1. Перенеси все файлы игры в эту папку! |
| 2. Запути bat файл "Запусти для создания exe" и подожди завершения экспорта. |
| 3. Твой экспорт будет находиться в этой же папке в dist/win-unpacked |
| `; |
|
|
| const packageJson = `{ |
| "name": "game", |
| "version": "1.0.0", |
| "main": "main.js", |
| "scripts": { |
| "start": "electron .", |
| "build": "electron-builder" |
| }, |
| "devDependencies": { |
| "electron": "^28.0.0", |
| "electron-builder": "^24.9.1" |
| } |
| }`; |
|
|
| const mainJs = `const { app, BrowserWindow } = require('electron'); |
|
|
| function createWindow() { |
| const win = new BrowserWindow({ |
| width: 1280, |
| height: 720, |
| minWidth: 1280, |
| minHeight: 720, |
| maxWidth: 1920, |
| maxHeight: 1080, |
| autoHideMenuBar: true, // Скрыть меню |
| frame: true, // Полноценный заголовок окна |
| webPreferences: { |
| nodeIntegration: true, |
| contextIsolation: false |
| } |
| }); |
| |
| win.setMenu(null); // Полное отключение меню |
| win.loadFile('game.html'); |
| |
| // Дополнительные настройки |
| win.on('ready-to-show', () => { |
| win.show(); |
| }); |
| } |
|
|
| app.whenReady().then(createWindow); |
|
|
| app.on('window-all-closed', () => { |
| if (process.platform !== 'darwin') app.quit(); |
| }); |
| `; |
|
|
| const zip = new JSZip(); |
| zip.file("game.html", htmlContent); |
| zip.file("package.json", packageJson); |
| zip.file("ПРОЧТИ ОБЯЗАТЕЛЬНО!.txt", readmeTxt); // Добавляем readme |
| zip.file("main.js", mainJs); |
| |
| // Добавляем BAT-файл с правильным именем |
| zip.file("Запусти для создания exe.bat", batContent, { |
| unixOptions: 0o644, |
| dosAttributes: 0x20 // Архивный атрибут |
| }); |
|
|
| zip.generateAsync({ type: "blob" }).then(blob => { |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'game-electron.zip'; |
| a.click(); |
| URL.revokeObjectURL(url); |
| }); |
| } |
|
|
| // Привязка кнопок экспорта |
| document.getElementById('btnExportExe').addEventListener('click', exportToExe); |
|
|
|
|
|
|
| // Привязка кнопки экспорта |
| document.getElementById('btnExport').addEventListener('click', exportToWeb); |
|
|
| // Создаем экземпляр Game после объявления scriptContent |
| const game = new Game(); |
| const treeEl = document.getElementById('tree'); |
| const btnNew = document.getElementById('btnNew'); |
| const btnOpen = document.getElementById('btnOpen'); |
| const btnSave = document.getElementById('btnSave'); |
| const fileInput = document.getElementById('fileInput'); |
| const btnPlay = document.getElementById('btnPlay'); |
|
|
| // Улучшенная подсветка синтаксиса |
| CodeMirror.defineMode("custom", function() { |
| return { |
| token: function(stream, state) { |
| if (stream.match(/^"[^"]*"/)) return "string"; |
| if (stream.match(/^\/\/.*/)) return "comment"; |
| |
| if (stream.sol()) { |
| if (stream.match(/^(loc|case|s|img|play|rn|if|else)\b/)) { |
| state.expecting = "keyword-content"; |
| return "keyword"; |
| } |
| if (stream.match(/^\((l|c|loc|case|m|меню)\b/)) { |
| state.inNavCommand = true; |
| return "nav-command"; |
| } |
| } |
| |
| if (state.expecting === "keyword-content") { |
| if (stream.match(/\S+/)) { |
| state.expecting = null; |
| return "keyword-content"; |
| } |
| } |
| |
| if (state.inNavCommand) { |
| if (stream.match(/\)/)) { |
| state.inNavCommand = false; |
| return "nav-command"; |
| } |
| if (stream.match(/[-\|]/)) return "nav-separator"; |
| return "nav-content"; |
| } |
| |
| if (stream.match(/^\{\w+\}/)) return "variable"; |
| if (stream.match(/[=><!+\-*/]/)) return "operator"; |
| if (stream.match(/[(){}]/)) return "bracket"; |
| if (stream.match(/^\d+/)) return "number"; |
| if (stream.match(/^(true|false)\b/)) return "boolean"; |
| if (stream.match(/^<[a-z][a-z0-9]*>/i)) return "html-tag"; |
| if (stream.match(/^<\/[a-z][a-z0-9]*>/i)) return "html-tag"; |
| |
| if (stream.eatSpace()) return null; |
| stream.next(); |
| return null; |
| }, |
| |
| startState: function() { |
| return { |
| expecting: null, |
| inNavCommand: false |
| }; |
| } |
| }; |
| }); |
|
|
| // Определяем команды для автодополнения |
| const commands = [ |
| "loc", "case", "s", "img", "play", "rn", "if", "else", "m", "меню", "l", "c", "item" |
| ]; |
|
|
| // Регистрируем помощник для автодополнения |
| CodeMirror.registerHelper("hint", "custom", function(editor) { |
| const cur = editor.getCursor(); |
| const token = editor.getTokenAt(cur); |
| const start = token.start; |
| const end = cur.ch; |
| const str = token.string; |
|
|
| if (str && commands.some(cmd => cmd.startsWith(str))) { |
| return { |
| list: commands.filter(cmd => cmd.startsWith(str)), |
| from: CodeMirror.Pos(cur.line, start), |
| to: CodeMirror.Pos(cur.line, end) |
| }; |
| } |
| }); |
|
|
| // Функция валидации для подсветки ошибок |
| function validateScript(text) { |
| const lines = text.split('\n'); |
| const errors = []; |
| let inLocation = false; |
|
|
| lines.forEach((line, index) => { |
| const trimmed = line.trim(); |
| if (trimmed.startsWith("loc ")) { |
| inLocation = true; |
| } else if (trimmed.startsWith("case ") && !inLocation) { |
| errors.push({ |
| line: index, |
| message: "Ошибка: 'case' вне 'loc'." |
| }); |
| } else if (inLocation && !trimmed.startsWith(" ") && !trimmed.startsWith("case ") && trimmed !== "") { |
| errors.push({ |
| line: index, |
| message: "Ошибка: Неверный формат или отступ." |
| }); |
| } |
| }); |
|
|
| return errors; |
| } |
|
|
| // Регистрируем помощник для подсветки ошибок |
| CodeMirror.registerHelper("lint", "custom", function(text) { |
| return validateScript(text).map(error => ({ |
| from: CodeMirror.Pos(error.line, 0), |
| to: CodeMirror.Pos(error.line, 0), |
| message: error.message |
| })); |
| }); |
|
|
| // Инициализируем редактор CodeMirror |
| const codeMirrorEditor = CodeMirror.fromTextArea(document.getElementById("editor"), { |
| lineNumbers: true, |
| indentUnit: 4, |
| tabSize: 4, |
| historyEventDelay: 0, |
| indentWithTabs: false, |
| mode: "custom", |
| extraKeys: { |
| "Ctrl-Space": "autocomplete", |
| "Tab": (cm) => { cm.replaceSelection(" ", "end"); }, |
| "Ctrl-Z": (cm) => { cm.undo(); }, |
| "Ctrl-S": (cm) => { saveProjectToFile(cm.getValue()); } |
| }, |
| lineWrapping: true, |
| lint: true |
| }); |
|
|
| // Определяем команду автодополнения |
| CodeMirror.commands.autocomplete = function(cm) { |
| cm.showHint({ hint: CodeMirror.hint.custom }); |
| }; |
|
|
| let currentEditType = 'all'; |
| let currentEditLoc = null; |
| let currentEditCase = null; |
|
|
| codeMirrorEditor.on("change", () => { |
| const newText = codeMirrorEditor.getValue(); |
| if (currentEditType === 'all') { |
| scriptContent = newText; |
| } else if (currentEditType === 'location') { |
| scriptContent = replaceLocationText(scriptContent, currentEditLoc, newText); |
| } else if (currentEditType === 'case') { |
| scriptContent = replaceCaseText(scriptContent, currentEditLoc, currentEditCase, newText); |
| } |
| updateTree(); |
| }); |
|
|
| function setEditorContent(text) { codeMirrorEditor.setValue(text); } |
|
|
| function extractLocationText(script, locName) { |
| const lines = script.split('\n'); |
| let startLine = -1; |
| let endLine = lines.length; |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i].trim(); |
| if (line.startsWith(`loc ${locName}`)) { |
| startLine = i; |
| } else if (line.startsWith('loc ') && startLine !== -1) { |
| endLine = i; |
| break; |
| } |
| } |
| if (startLine === -1) return ''; |
| return lines.slice(startLine, endLine).join('\n'); |
| } |
|
|
| function extractCaseText(script, locName, caseName) { |
| const locText = extractLocationText(script, locName); |
| if (!locText) return ''; |
| const lines = locText.split('\n'); |
| let startLine = -1; |
| let endLine = lines.length; |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i].trim(); |
| if (line.startsWith(`case ${caseName}`)) { |
| startLine = i; |
| } else if (line.startsWith('case ') && startLine !== -1) { |
| endLine = i; |
| break; |
| } |
| } |
| if (startLine === -1) return ''; |
| return lines.slice(startLine, endLine).join('\n'); |
| } |
|
|
| function replaceLocationText(script, locName, newText) { |
| const lines = script.split('\n'); |
| let startLine = -1; |
| let endLine = lines.length; |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i].trim(); |
| if (line.startsWith(`loc ${locName}`)) { |
| startLine = i; |
| } else if (line.startsWith('loc ') && startLine !== -1) { |
| endLine = i; |
| break; |
| } |
| } |
| if (startLine === -1) return script; |
| const before = lines.slice(0, startLine).join('\n'); |
| const after = lines.slice(endLine).join('\n'); |
| return before + (before ? '\n' : '') + newText + (after ? '\n' : '') + after; |
| } |
|
|
| function replaceCaseText(script, locName, caseName, newText) { |
| const locText = extractLocationText(script, locName); |
| if (!locText) return script; |
| const lines = locText.split('\n'); |
| let startLine = -1; |
| let endLine = lines.length; |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i].trim(); |
| if (line.startsWith(`case ${caseName}`)) { |
| startLine = i; |
| } else if (line.startsWith('case ') && startLine !== -1) { |
| endLine = i; |
| break; |
| } |
| } |
| if (startLine === -1) return script; |
| const beforeCase = lines.slice(0, startLine).join('\n'); |
| const afterCase = lines.slice(endLine).join('\n'); |
| const newLocText = beforeCase + (beforeCase ? '\n' : '') + newText + (afterCase ? '\n' : '') + afterCase; |
| return replaceLocationText(script, locName, newLocText); |
| } |
|
|
| function updateTree() { |
| treeEl.innerHTML = ''; |
| let root = document.createElement('li'); |
| root.textContent = "Локации"; |
| root.classList.add("active"); |
| root.addEventListener('click', (e) => { |
| currentEditType = 'all'; |
| currentEditLoc = null; |
| currentEditCase = null; |
| setEditorContent(scriptContent); |
| Array.from(treeEl.getElementsByTagName('li')).forEach(item => item.classList.remove("active")); |
| root.classList.add("active"); |
| }); |
| treeEl.appendChild(root); |
|
|
| let locs = game.parseLocations(scriptContent); |
| for (let loc in locs) { |
| if (loc === "переменные") continue; |
| let locItem = document.createElement('li'); |
| locItem.dataset.loc = loc; |
| let toggleBtn = document.createElement('span'); |
| toggleBtn.className = "toggle"; |
| toggleBtn.textContent = "+"; |
| toggleBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| let ul = locItem.querySelector("ul"); |
| if (ul) { |
| if (ul.style.display === "block") { ul.style.display = "none"; toggleBtn.textContent = "+"; } |
| else { ul.style.display = "block"; toggleBtn.textContent = "–"; } |
| } |
| }); |
| locItem.prepend(toggleBtn); |
| locItem.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| currentEditType = 'location'; |
| currentEditLoc = loc; |
| currentEditCase = null; |
| const locText = extractLocationText(scriptContent, loc); |
| setEditorContent(locText); |
| Array.from(treeEl.getElementsByTagName('li')).forEach(item => item.classList.remove("active")); |
| locItem.classList.add("active"); |
| }); |
| locItem.appendChild(document.createTextNode(" " + loc)); |
| let caseList = document.createElement('ul'); |
| for (let cs in locs[loc].cases) { |
| let csItem = document.createElement('li'); |
| csItem.dataset.loc = loc; |
| csItem.dataset.case = cs; |
| csItem.textContent = cs; |
| csItem.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| currentEditType = 'case'; |
| currentEditLoc = loc; |
| currentEditCase = cs; |
| const caseText = extractCaseText(scriptContent, loc, cs); |
| setEditorContent(caseText); |
| Array.from(treeEl.getElementsByTagName('li')).forEach(item => item.classList.remove("active")); |
| csItem.classList.add("active"); |
| }); |
| caseList.appendChild(csItem); |
| } |
| locItem.appendChild(caseList); |
| treeEl.appendChild(locItem); |
| } |
| } |
|
|
| btnNew.addEventListener('click', () => { |
| if (confirm("Создать новый проект? Текущие изменения будут потеряны.")) { |
| scriptContent = |
| `loc начало |
| case 0 |
| (m (l Начало игры - дом) (c Об авторе - инфо)) |
| case инфо |
| (m (c назад - 0)) |
| loc дом |
| case 0 |
| Сейчас {hour}:{min} |
| (item if knife == false (На столе лежит (Нож| s knife = true))) |
| loc инвентарь |
| case 0 |
| У меня {gold} золота |
| Меня зовут {name} |
| if knife == true (У меня Нож) // |
| if gun == true (У меня Пистолет) // |
| loc переменные |
| s gold = 100 |
| s name = "Андрей" |
| s gun = false |
| s knife = false |
| s book = false |
| s year = 1950 |
| s month = 6 |
| s day = 2 |
| s hour = 10 |
| s min = 7 |
| s clik = 10 |
| `; |
| setEditorContent(scriptContent); |
| updateTree(); |
| } |
| }); |
|
|
| btnOpen.addEventListener('click', () => fileInput.click()); |
|
|
| fileInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| const reader = new FileReader(); |
| reader.onload = (event) => { |
| scriptContent = event.target.result; |
| setEditorContent(scriptContent); |
| updateTree(); |
| }; |
| reader.readAsText(file, "UTF-8"); |
| fileInput.value = ""; |
| }); |
|
|
| async function saveProjectToFile(content) { |
| if (window.showSaveFilePicker) { |
| try { |
| const opts = { types: [{ description: 'VQ Project Files', accept: {'text/plain': ['.txt']} }] }; |
| const handle = await window.showSaveFilePicker(opts); |
| const writable = await handle.createWritable(); |
| await writable.write(content); |
| await writable.close(); |
| alert("Проект сохранён!"); |
| } catch (err) { alert("Ошибка сохранения: " + err.message); } |
| } else { |
| const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); |
| const a = document.createElement("a"); |
| a.href = URL.createObjectURL(blob); |
| a.download = "project.txt"; |
| a.click(); |
| URL.revokeObjectURL(a.href); |
| alert("Проект сохранён!"); |
| } |
| } |
|
|
| btnSave.addEventListener('click', async () => { |
| await saveProjectToFile(scriptContent); |
| }); |
|
|
| btnPlay.addEventListener('click', () => { |
| document.getElementById('game-container').style.display = 'flex'; |
| document.getElementById('game-container').style.flexDirection = 'column'; |
| document.getElementById('overlay').style.display = 'flex'; |
| game.loadScriptFromText(scriptContent); |
| }); |
|
|
| document.getElementById('stop-game').addEventListener('click', () => { |
| document.getElementById('overlay').style.display = 'none'; |
| document.getElementById('game-container').style.display = 'none'; |
| }); |
|
|
|
|
| updateTree(); |
| </script> |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/hint/show-hint.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/addon/lint/lint.min.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> |
|
|
| <input type="file" id="load-input" style="display:none;" accept=".json" /> |
| </body> |
| </html> |