deepsite / index.html
R2ot's picture
Add 2 files
ff850fe verified
Raw
History Blame Contribute Delete
54 kB
<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 = "";
// Класс Game для управления игрой
class Game {
constructor() {
this.content = "";
this.state = {};
this.locations = {};
this.currentLocation = 'начало';
this.currentCase = '0';
this.audio = null;
this.cacheElements();
this.initEventListeners();
}
// Кэширование элементов DOM
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();
}
}
});
}
}
}
// Инициализация редактора CodeMirror
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
});
// Функция экспорта в Web-формат
function exportToWeb() {
// Получаем содержимое из редактора CodeMirror
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>