Compare commits

...

12 Commits

Author SHA1 Message Date
Mikhail 3683fcff68 бэкап-снимок: прод-правки grechka-game 2026-10-10 00:10:47 +03:00
hermes d008995a41 v3: полностью 3D мир (генерация по XYZ, физика по 3 осям, добыча по близости) 2026-09-27 00:09:10 +03:00
Mk 547ad86989 feat: minecraft-style start menu with proper spacing 2026-05-27 04:54:39 +00:00
Mk f14b61d7d9 feat: nickname system + friend requests + privacy settings
- Nicknames: unique, 2-16 chars, alphanumeric + russian + underscore
- Friend requests: send by nickname, accept/decline with confirmation
- No raw TG IDs exposed — only nicknames
- Privacy: allow_requests toggle, notify_online toggle
- Bot: /code shows nickname, /add <nick> sends request, /friends shows list + requests
- Client: full friends UI with requests, accept/decline, nickname setup
2026-05-27 04:30:52 +00:00
Mk 3733b4b94f feat: friends system, clipboard toast, mob sync fix, player lerp
- Clipboard: click worldId copies code + toast notification (fallback for no-clipboard)
- Friends: tg_friends table, /api/tg/friends endpoints, bot /friends /addfriend /myid commands
- Online notifications: notify friends via bot when player joins world
- Mob sync: server-authoritative — client NO longer runs mobAI for serverMobs
- Mob spawn Y: surfaceGyAt recalculated on client (fix: mobs falling from sky / stuck in ground)
- Server mob physics: sgy-1 floor, AI per mob type (hostile chase at night, passive wander)
- Player positions: lerp (0.25) for smooth other-player rendering
- tg_online table for friend online tracking
- Offline cleanup on disconnect
2026-05-26 20:54:29 +00:00
Mk 528a698cf0 feat: TG Mini App + start menu (new world, join by code, continue game) + short world codes + TG save/load + share invites 2026-05-26 20:33:39 +00:00
Mk 42f3e59a42 fix: voice chat (addSpeaker to worklet, sendPCMFrame scope, VAD threshold, gain=1), temp damage underground + reduced speed 2026-05-26 20:20:26 +00:00
Mk 79fc30945a fix: register remote speaker in playback worklet (addSpeaker) — voice chat was silent because worklet never knew about speakers 2026-05-26 19:40:17 +00:00
Mk eaef5b2205 fix: server-authoritative mob sync, seed sync, genColumn waits for seed 2026-05-26 19:05:24 +00:00
Mk a5f9ac841f fix: subtle torch/campfire flicker, stable warm glow overlay 2026-05-26 18:16:58 +00:00
Mk d948b1743d feat: temperature system + XP on loot pickup 2026-05-26 18:10:25 +00:00
Mk 94e6f535d0 fix: update XP/Level HUD in game loop (was only on connect) 2026-05-26 17:56:27 +00:00
214 changed files with 51172 additions and 21792 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
backup/
backup_v*/
*.bak*
node_modules/

View File

@ -4,6 +4,12 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/style.css
COPY game.js /usr/share/nginx/html/game.js
COPY three147.min.js /usr/share/nginx/html/three147.min.js
COPY game3d.js /usr/share/nginx/html/game3d.js
COPY tex/ /usr/share/nginx/html/tex/
COPY index-v2.html /usr/share/nginx/html/v2.html
COPY manifest.json /usr/share/nginx/html/manifest.json
COPY sw.js /usr/share/nginx/html/sw.js
COPY src/ /usr/share/nginx/html/src/
EXPOSE 80

Binary file not shown.

View File

@ -1,11 +0,0 @@
FROM nginx:alpine
# Копируем файлы игры в директорию nginx
COPY index.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/style.css
COPY game.js /usr/share/nginx/html/game.js
# Используем конфигурацию nginx по умолчанию
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
FROM nginx:alpine
# Копируем файлы игры в директорию nginx
COPY index.html /usr/share/nginx/html/index.html
COPY style.css /usr/share/nginx/html/style.css
COPY game.js /usr/share/nginx/html/game.js
# Используем конфигурацию nginx по умолчанию
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
name: grechka-game
services:
grechka-game:
build:
context: .
dockerfile: Dockerfile
container_name: grechka-game
restart: unless-stopped
networks:
- shared_network
labels:
# Включаем Traefik для этого сервиса
- "traefik.enable=true"
- "traefik.docker.network=shared_network"
# HTTPS роутер
- "traefik.http.routers.grechka-game.entrypoints=websecure"
- "traefik.http.routers.grechka-game.rule=Host(`grechka.mkn8n.ru`)"
- "traefik.http.routers.grechka-game.tls=true"
- "traefik.http.routers.grechka-game.tls.certresolver=mytlschallenge"
# Сервис
- "traefik.http.services.grechka-game.loadbalancer.server.port=80"
networks:
shared_network:
external: true

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>GrechkaCraft: Multiplayer</title>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">🏃</div>
<div id="saveBtn" class="rbtn pe">💾</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="resetBtn" class="rbtn pe">🔄</div>
<div id="craftPanel">
<div class="top">
<div style="font-weight:900;font-size:18px;">Крафт</div>
<button class="close" id="craftClose">Закрыть</button>
</div>
<div id="recipes"></div>
</div>
<div id="inventoryPanel">
<div class="top">
<div style="font-weight:900;font-size:18px;">Инвентарь</div>
<button class="close" id="inventoryClose">Закрыть</button>
</div>
<div id="inventoryGrid"></div>
</div>
<div id="death">
<div style="font-size:22px;font-weight:900;">ВЫ ПОГИБЛИ</div>
<button id="respawnBtn">ВОЗРОДИТЬСЯ</button>
</div>
<div id="hotbar" class="pe"></div>
<div id="invToggle" class="rbtn pe" style="bottom:10px; left:10px; background:#3498db;">🎒</div>
</div>
<!-- Chat UI -->
<div id="chatPanel" style="display:none; position:absolute; left:10px; right:10px; top:60px; bottom:160px; background:rgba(0,0,0,0.85); border:2px solid rgba(255,255,255,0.8); border-radius:12px; pointer-events:auto; z-index:50; padding:10px; overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px;">
<div style="font-weight:900;font-size:18px;">💬 Чат</div>
<button id="chatClose" style="background:#c0392b; border:none; color:#fff; font-weight:900; padding:6px 10px; border-radius:8px; cursor:pointer;">✕</button>
</div>
<div id="chatMessages" style="flex:1; overflow-y:auto; color:#fff; font-size:13px; margin-bottom:8px; max-height:calc(100% - 40px);"></div>
<div style="display:flex; gap:6px;">
<input id="chatInput" type="text" placeholder="Введите сообщение..." style="flex:1; padding:8px; border:2px solid rgba(255,255,255,0.5); border-radius:8px; background:rgba(255,255,255,0.1); color:#fff; font-size:13px;" />
<button id="chatSend" style="background:#2ecc71; border:none; color:#fff; font-weight:900; padding:8px 16px; border-radius:8px; cursor:pointer;">➤</button>
</div>
</div>
<div id="chatToggle" class="rbtn pe">💬</div>
</div>
<div id="controls">
<div id="left" class="cbtn">←</div>
<div id="right" class="cbtn">→</div>
<div id="jump" class="cbtn">↑</div>
</div>
<script src="game.js"></script>
</body>
</html>

View File

@ -1,27 +0,0 @@
name: grechka-game
services:
grechka-game:
build:
context: .
dockerfile: Dockerfile
container_name: grechka-game
restart: unless-stopped
networks:
- shared_network
labels:
# Включаем Traefik для этого сервиса
- "traefik.enable=true"
- "traefik.docker.network=shared_network"
# HTTPS роутер
- "traefik.http.routers.grechka-game.entrypoints=websecure"
- "traefik.http.routers.grechka-game.rule=Host(`grechka.mkn8n.ru`)"
- "traefik.http.routers.grechka-game.tls=true"
- "traefik.http.routers.grechka-game.tls.certresolver=mytlschallenge"
# Сервис
- "traefik.http.services.grechka-game.loadbalancer.server.port=80"
networks:
shared_network:
external: true

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>GrechkaCraft: Multiplayer</title>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">🏃</div>
<div id="saveBtn" class="rbtn pe">💾</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="resetBtn" class="rbtn pe">🔄</div>
<div id="craftPanel">
<div class="top">
<div style="font-weight:900;font-size:18px;">Крафт</div>
<button class="close" id="craftClose">Закрыть</button>
</div>
<div id="recipes"></div>
</div>
<div id="inventoryPanel">
<div class="top">
<div style="font-weight:900;font-size:18px;">Инвентарь</div>
<button class="close" id="inventoryClose">Закрыть</button>
</div>
<div id="inventoryGrid"></div>
</div>
<div id="death">
<div style="font-size:22px;font-weight:900;">ВЫ ПОГИБЛИ</div>
<button id="respawnBtn">ВОЗРОДИТЬСЯ</button>
</div>
<div id="hotbar" class="pe"></div>
<div id="invToggle" class="rbtn pe" style="bottom:10px; left:10px; background:#3498db;">🎒</div>
</div>
<!-- Chat UI -->
<div id="chatPanel" style="display:none; position:absolute; left:10px; right:10px; top:60px; bottom:160px; background:rgba(0,0,0,0.85); border:2px solid rgba(255,255,255,0.8); border-radius:12px; pointer-events:auto; z-index:50; padding:10px; overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px;">
<div style="font-weight:900;font-size:18px;">💬 Чат</div>
<button id="chatClose" style="background:#c0392b; border:none; color:#fff; font-weight:900; padding:6px 10px; border-radius:8px; cursor:pointer;">✕</button>
</div>
<div id="chatMessages" style="flex:1; overflow-y:auto; color:#fff; font-size:13px; margin-bottom:8px; max-height:calc(100% - 40px);"></div>
<div style="display:flex; gap:6px;">
<input id="chatInput" type="text" placeholder="Введите сообщение..." style="flex:1; padding:8px; border:2px solid rgba(255,255,255,0.5); border-radius:8px; background:rgba(255,255,255,0.1); color:#fff; font-size:13px;" />
<button id="chatSend" style="background:#2ecc71; border:none; color:#fff; font-weight:900; padding:8px 16px; border-radius:8px; cursor:pointer;">➤</button>
</div>
</div>
<div id="chatToggle" class="rbtn pe">💬</div>
</div>
<div id="controls">
<div id="left" class="cbtn">←</div>
<div id="right" class="cbtn">→</div>
<div id="jump" class="cbtn">↑</div>
</div>
<script src="game.js"></script>
</body>
</html>

View File

@ -1,72 +0,0 @@
/* Minegrechka Game Styles - v1.2 */
html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#111; font-family: system-ui, sans-serif; user-select:none; -webkit-user-select:none; touch-action:none; }
#game { position:absolute; top:0; left:0; right:0; bottom:140px; background:#87CEEB; overflow:hidden; }
canvas { display:block; width:100%; height:100%; image-rendering:pixelated; }
#controls { position:absolute; left:0; right:0; bottom:0; height:140px; background:#222; border-top:4px solid #444; z-index:10; }
.ui { position:absolute; inset:0; pointer-events:none; z-index:20; }
.pe { pointer-events:auto; }
#stats { position:absolute; left:10px; top:10px; color:#fff; font-weight:800; font-size:14px;
background: rgba(0,0,0,0.55); padding:8px; border-radius:10px; text-shadow:1px 1px 0 #000; }
#stats .row{ display:flex; gap:10px; align-items:center; }
.rbtn { position:absolute; right:10px; width:52px; height:52px; border-radius:12px;
display:flex; align-items:center; justify-content:center; border:2px solid rgba(255,255,255,0.9);
font-size:24px; cursor:pointer; pointer-events:auto; box-shadow:0 4px 0 rgba(0,0,0,0.5); }
.rbtn:active { transform: translateY(4px); box-shadow:none; }
#modeBtn { top:10px; background:#f39c12; }
#saveBtn { top:10px; right:70px !important; background:#27ae60; }
#resetBtn { top:10px; right:130px !important; background:#e74c3c; }
#craftBtn { top:74px; right:10px !important; background:#9b59b6; }
#chatToggle { top:10px; right:190px !important; background:#9b59b6; }
#hotbar { position:absolute; left:50%; transform:translateX(-50%); bottom:10px; display:flex; gap:6px;
background: rgba(0,0,0,0.60); padding:6px; border-radius:12px; pointer-events:auto;
overflow-x: auto; overflow-y: hidden; max-width: 80%; }
.slot { width:38px; height:38px; border:2px solid rgba(255,255,255,0.22); border-radius:10px;
position:relative; overflow:hidden; cursor:pointer; background: rgba(255,255,255,0.07);
display:flex; align-items:center; justify-content:center; font-size:18px; }
.slot.sel { border-color:#f1c40f; box-shadow: 0 0 0 2px rgba(241,196,15,0.18) inset; }
.count { position:absolute; right:3px; bottom:1px; font-size:10px; color:#fff; font-weight:900; text-shadow:1px 1px 0 #000; }
/* Craft modal */
#craftPanel { display:none; position:absolute; left:14px; right:14px; top:14px; bottom:14px;
background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85); border-radius:14px;
pointer-events:auto; padding:12px; overflow:auto; }
/* Inventory modal */
#inventoryPanel { display:none; position:absolute; left:50%; top:50%; transform: translate(-50%, -50%);
width: 420px; max-width: 90%; background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85);
border-radius:14px; pointer-events:auto; padding:12px; z-index: 100; }
#inventoryPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; }
#inventoryGrid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-top: 10px; }
.inv-slot { width: 48px; height: 48px; border: 2px solid rgba(255,255,255,0.22); border-radius: 8px;
position: relative; overflow: hidden; cursor: pointer; background: rgba(255,255,255,0.07);
display: flex; align-items: center; justify-content: center; font-size: 24px; }
.inv-slot.sel { border-color: #f1c40f; box-shadow: 0 0 0 2px rgba(241,196,15,0.18) inset; }
.inv-count { position: absolute; right: 3px; bottom: 1px; font-size: 12px; color: #fff;
font-weight: 900; text-shadow: 1px 1px 0 #000; background: rgba(0,0,0,0.5); padding: 1px 3px; border-radius: 4px; }
#craftPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; }
#craftPanel .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; }
.recipe { display:flex; align-items:center; gap:10px; padding:10px; border-radius:12px;
background: rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.10); margin-bottom:8px; }
.ricon { width:32px; height:32px; border-radius:8px; background-size:cover; image-rendering:pixelated; }
.rinfo { flex:1; }
.rname { color:#fff; font-weight:900; font-size:14px; }
.rcost { color:#bbb; font-size:11px; line-height:1.25; }
.rcraft { background:#2ecc71; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; }
.rcraft:disabled { background:#444; color:#888; cursor:not-allowed; }
/* Controls buttons */
.cbtn { position:absolute; top:50%; transform:translateY(-50%); width:74px; height:74px; border-radius:14px;
background:#333; border:3px solid #555; color:#fff; font-size:32px; display:flex; align-items:center; justify-content:center;
box-shadow:0 7px 0 #111; pointer-events:auto; }
.cbtn:active { transform:translateY(-46%); box-shadow:0 3px 0 #111; background:#444; }
#left { left:18px; }
#right { left:102px; }
#jump { right:18px; background:#d35400; border-color:#e67e22; }
#death { display:none; position:absolute; inset:0; background: rgba(60,0,0,0.88);
z-index:200; color:#fff; pointer-events:auto; align-items:center; justify-content:center; flex-direction:column; gap:12px; }
#death button { padding:12px 18px; font-size:18px; font-weight:900; border:none; border-radius:12px; cursor:pointer; }

0
build.err Normal file
View File

107
build.js Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Simple ES-module → IIFE bundler for GrechkaCraft
* Resolves import/export, wraps in single IIFE, no external deps.
* Usage: node build.js > game.js
*/
const fs = require('fs');
const path = require('path');
const SRC = path.join(__dirname, 'src');
const ENTRY = 'main.js';
const visited = new Set();
const chunks = [];
const fileExports = {};
const fileImports = {};
function resolveImportPath(fromPath, importerDir) {
let resolved = path.resolve(importerDir, fromPath);
if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.js')) {
resolved += '.js';
}
return resolved;
}
function collectExports(content) {
const exports = [];
const re1 = /export\s*\{([^}]+)\}/g;
let m;
while ((m = re1.exec(content)) !== null) {
const names = m[1].split(',').map(s => {
const parts = s.trim().split(/\s+as\s+/);
return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() };
});
exports.push(...names);
}
const re2 = /export\s+(?:async\s+)?(?:const|let|var|function|class)\s+(\w+)/g;
while ((m = re2.exec(content)) !== null) {
exports.push({ local: m[1], exported: m[1] });
}
if (/export\s+default\s+/.test(content)) {
exports.push({ local: '__default__', exported: 'default' });
}
return exports;
}
function collectImports(content) {
const imports = [];
const re = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
let m;
while ((m = re.exec(content)) !== null) {
const names = m[1].split(',').map(s => {
const parts = s.trim().split(/\s+as\s+/);
return { local: (parts[1] || parts[0]).trim(), imported: parts[0].trim() };
});
imports.push({ names, from: m[2] });
}
const re2 = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
while ((m = re2.exec(content)) !== null) {
imports.push({ names: [{ local: m[1], imported: 'default' }], from: m[2] });
}
return imports;
}
function processFile(filePath) {
const relPath = path.relative(SRC, filePath);
if (visited.has(relPath)) return;
visited.add(relPath);
let content = fs.readFileSync(filePath, 'utf8');
const dir = path.dirname(filePath);
fileExports[relPath] = collectExports(content);
fileImports[relPath] = collectImports(content);
for (const imp of fileImports[relPath]) {
const depPath = resolveImportPath(imp.from, dir);
processFile(depPath);
}
// Strip import statements
content = content.replace(/import\s*\{[^}]+\}\s*from\s*['"][^'"]+['"];?/g, '');
content = content.replace(/import\s+\w+\s+from\s*['"][^'"]+['"];?/g, '');
// Strip export keyword but keep declarations
content = content.replace(/export\s+default\s+/, 'const __default__ = ');
content = content.replace(/export\s+(?:async\s+)?(const|let|var|function|class)\s/g, '$1 ');
content = content.replace(/export\s*\{[^}]*\};?/g, '');
chunks.push({ relPath, content });
}
processFile(path.join(SRC, ENTRY));
let output = '// GrechkaCraft — auto-bundled from ES modules\n';
output += '(function() {\n';
output += '"use strict";\n\n';
for (const chunk of chunks) {
output += `// === ${chunk.relPath} ===\n`;
output += chunk.content.trim();
output += '\n\n';
}
output += '})();\n';
process.stdout.write(output);

107
build3d.js Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env node
/**
* Simple ES-module → IIFE bundler for GrechkaCraft
* Resolves import/export, wraps in single IIFE, no external deps.
* Usage: node build.js > game.js
*/
const fs = require('fs');
const path = require('path');
const SRC = path.join(__dirname, 'srcv3');
const ENTRY = 'main3d.js';
const visited = new Set();
const chunks = [];
const fileExports = {};
const fileImports = {};
function resolveImportPath(fromPath, importerDir) {
let resolved = path.resolve(importerDir, fromPath);
if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.js')) {
resolved += '.js';
}
return resolved;
}
function collectExports(content) {
const exports = [];
const re1 = /export\s*\{([^}]+)\}/g;
let m;
while ((m = re1.exec(content)) !== null) {
const names = m[1].split(',').map(s => {
const parts = s.trim().split(/\s+as\s+/);
return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() };
});
exports.push(...names);
}
const re2 = /export\s+(?:async\s+)?(?:const|let|var|function|class)\s+(\w+)/g;
while ((m = re2.exec(content)) !== null) {
exports.push({ local: m[1], exported: m[1] });
}
if (/export\s+default\s+/.test(content)) {
exports.push({ local: '__default__', exported: 'default' });
}
return exports;
}
function collectImports(content) {
const imports = [];
const re = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
let m;
while ((m = re.exec(content)) !== null) {
const names = m[1].split(',').map(s => {
const parts = s.trim().split(/\s+as\s+/);
return { local: (parts[1] || parts[0]).trim(), imported: parts[0].trim() };
});
imports.push({ names, from: m[2] });
}
const re2 = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
while ((m = re2.exec(content)) !== null) {
imports.push({ names: [{ local: m[1], imported: 'default' }], from: m[2] });
}
return imports;
}
function processFile(filePath) {
const relPath = path.relative(SRC, filePath);
if (visited.has(relPath)) return;
visited.add(relPath);
let content = fs.readFileSync(filePath, 'utf8');
const dir = path.dirname(filePath);
fileExports[relPath] = collectExports(content);
fileImports[relPath] = collectImports(content);
for (const imp of fileImports[relPath]) {
const depPath = resolveImportPath(imp.from, dir);
processFile(depPath);
}
// Strip import statements
content = content.replace(/import\s*\{[^}]+\}\s*from\s*['"][^'"]+['"];?/g, '');
content = content.replace(/import\s+\w+\s+from\s*['"][^'"]+['"];?/g, '');
// Strip export keyword but keep declarations
content = content.replace(/export\s+default\s+/, 'const __default__ = ');
content = content.replace(/export\s+(?:async\s+)?(const|let|var|function|class)\s/g, '$1 ');
content = content.replace(/export\s*\{[^}]*\};?/g, '');
chunks.push({ relPath, content });
}
processFile(path.join(SRC, ENTRY));
let output = '// GrechkaCraft — auto-bundled from ES modules\n';
output += '(function() {\n';
output += '"use strict";\n\n';
for (const chunk of chunks) {
output += `// === ${chunk.relPath} ===\n`;
output += chunk.content.trim();
output += '\n\n';
}
output += '})();\n';
process.stdout.write(output);

11360
game.js

File diff suppressed because it is too large Load Diff

5865
game.js.bak.v30 Normal file

File diff suppressed because it is too large Load Diff

5190
game.js_new Normal file

File diff suppressed because it is too large Load Diff

8487
game3d.js Normal file

File diff suppressed because it is too large Load Diff

230
index-v2.html Normal file
View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#1a1a2e" />
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="imgs/icon-192.png" />
<title>GrechkaCraft</title>
<!-- Telegram WebApp -->
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=7">
<!-- Заставка появляется МГНОВЕННО, до загрузки game3d.js. Без неё браузер
показывает белый экран, пока тянется JS-бандл на 300 КБ. -->
<div id="v3boot" style="position:fixed;inset:0;z-index:2147483000;overflow:hidden;
background:linear-gradient(180deg,#0d1b3e 0%,#1b3a6b 42%,#3f6fa8 68%,#8fc7f5 100%);
font:600 15px/1.4 system-ui,-apple-system,Segoe UI,sans-serif;color:#eaf3ff;
display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center">
<div style="position:absolute;inset:0;overflow:hidden;pointer-events:none">
<div style="position:absolute;left:50%;top:26%;width:170px;height:170px;margin:-85px 0 0 -85px;
border-radius:50%;background:radial-gradient(circle,#fff6d8 0%,#ffe08a 42%,rgba(255,214,120,0) 72%);
animation:v3sun 6s ease-in-out infinite"></div>
<div style="position:absolute;top:16%;left:-30%;width:52%;height:40px;border-radius:50%;
background:radial-gradient(ellipse,#ffffff8a 0%,#ffffff3d 56%,rgba(255,255,255,0) 78%);filter:blur(3px);animation:v3cloud 40s linear infinite"></div>
<div style="position:absolute;top:27%;left:-40%;width:38%;height:26px;border-radius:50%;
background:radial-gradient(ellipse,#ffffff70 0%,#ffffff30 56%,rgba(255,255,255,0) 78%);filter:blur(3px);animation:v3cloud 58s linear infinite"></div>
<div style="position:absolute;left:-10%;bottom:0;width:120%;height:38%;border-radius:50% 50% 0 0;
background:linear-gradient(180deg,#2f7a3d,#1d5427)"></div>
<div style="position:absolute;left:6%;bottom:0;width:80%;height:26%;border-radius:50% 50% 0 0;
background:linear-gradient(180deg,#3a8f47,#256030)"></div>
<div style="position:absolute;left:50%;bottom:15%;margin-left:-11px;width:22px;height:56px;animation:v3bob 2.4s ease-in-out infinite"><div style="position:absolute;left:9px;bottom:0;width:4px;height:46px;background:#7dbf4a;border-radius:2px 2px 0 0"></div><div style="position:absolute;left:1px;bottom:38px;width:10px;height:10px;background:#e8c95c;border-radius:50% 50% 50% 0"></div><div style="position:absolute;left:12px;bottom:38px;width:10px;height:10px;background:#e8c95c;border-radius:50% 50% 0 50%"></div><div style="position:absolute;left:3px;bottom:29px;width:9px;height:9px;background:#dcbb4e;border-radius:50% 50% 50% 0"></div><div style="position:absolute;left:11px;bottom:29px;width:9px;height:9px;background:#dcbb4e;border-radius:50% 50% 0 50%"></div><div style="position:absolute;left:4px;bottom:20px;width:8px;height:8px;background:#cfae42;border-radius:50% 50% 50% 0"></div><div style="position:absolute;left:11px;bottom:20px;width:8px;height:8px;background:#cfae42;border-radius:50% 50% 0 50%"></div><div style="position:absolute;left:7px;bottom:44px;width:9px;height:11px;background:#dfc163;border-radius:50% 50% 45% 45%"></div></div><div style="width:8px;height:8px;margin:-38px -8px 0 0;background:#dcbb4e;border-radius:50% 50% 50% 0;transform:rotate(-40deg)"></div><div style="width:8px;height:8px;margin:-32px 0 0 0;background:#dcbb4e;border-radius:50% 50% 0 50%;transform:rotate(40deg)"></div><div style="width:7px;height:7px;margin:-26px -7px 0 0;background:#cfae42;border-radius:50% 50% 50% 0;transform:rotate(-40deg)"></div><div style="width:7px;height:7px;margin:-20px 0 0 0;background:#cfae42;border-radius:50% 50% 0 50%;transform:rotate(40deg)"></div></div>
</div>
<style>
@keyframes v3sun{0%,100%{transform:scale(1);opacity:.9}50%{transform:scale(1.08);opacity:1}}
@keyframes v3cloud{from{transform:translateX(0)}to{transform:translateX(280%)}}
@keyframes v3bob{0%,100%{transform:translate(-50%,0)}50%{transform:translate(-50%,-10px)}}
@keyframes v3spin{to{transform:rotate(360deg)}}
</style>
<div style="position:relative;display:flex;flex-direction:column;align-items:center;gap:16px">
<div style="font-size:clamp(22px,5vw,34px);font-weight:800;letter-spacing:.5px;
text-shadow:0 3px 14px #0007">Гречка Craft 3D</div>
<div id="v3bootstage" style="font-size:13px;opacity:.82;min-height:19px">Загружаем игру…</div>
<div style="width:min(330px,74vw);height:12px;border-radius:8px;background:#ffffff2b;overflow:hidden;
box-shadow:inset 0 1px 3px #0005,0 6px 18px #0004">
<div id="v3bootfill" style="width:0%;height:100%;border-radius:8px;
background:linear-gradient(90deg,#7ee08a,#2ecc71);box-shadow:0 0 12px #7ee08aaa;
transition:width .3s ease-out"></div></div>
<div id="v3boottxt" style="font-size:12px;opacity:.7">0%</div>
</div>
</div>
<script>
// Плавно доводим полосу до ~90%: реальный прогресс начнётся, когда загрузится
// game3d.js. Дальше его перехватит экран генерации мира.
(function(){
var f=document.getElementById('v3bootfill'), t=document.getElementById('v3boottxt');
var p=0, stop=false;
window.__v3bootDone=function(){ stop=true; };
var iv=setInterval(function(){
if(stop) return clearInterval(iv);
p=Math.min(90, p + (p<40?6:p<75?3:1.2));
if(f) f.style.width=p+'%';
if(t) t.textContent=Math.round(p)+'%';
},140);
setTimeout(function(){ if(!stop){ p=92; if(f)f.style.width='92%'; if(t)t.textContent='92%'; } },6000);
})();
</script>
<style>
/* ===== Джойстик для 3D-режима (только /v2, style.css не трогаем) ===== */
#v2joy{position:fixed;left:18px;bottom:22px;width:132px;height:132px;z-index:120;
border-radius:50%;pointer-events:auto;touch-action:none;
background:radial-gradient(circle at 50% 50%,rgba(255,255,255,.16),rgba(0,0,0,.30) 70%);
border:2px solid rgba(255,255,255,.34);display:none}
#v2joy.on{display:block}
#v2joy .knob{position:absolute;left:50%;top:50%;width:58px;height:58px;margin:-29px 0 0 -29px;
border-radius:50%;background:rgba(255,255,255,.60);border:2px solid rgba(255,255,255,.85);
box-shadow:0 3px 10px rgba(0,0,0,.4);transition:background .1s}
#v2joy.act .knob{background:rgba(120,220,255,.80)}
#v2jump{position:fixed;right:16px;bottom:26px;width:86px;height:86px;z-index:120;border-radius:50%;
display:none;align-items:center;justify-content:center;font-size:34px;pointer-events:auto;
touch-action:none;background:rgba(50,140,255,.42);border:2px solid rgba(255,255,255,.62);color:#fff}
#v2jump.on{display:flex}
#v2jump:active{background:rgba(50,140,255,.75)}
#v2mine{position:fixed;right:112px;bottom:32px;width:70px;height:70px;z-index:120;border-radius:50%;
display:none;align-items:center;justify-content:center;font-size:28px;pointer-events:auto;
touch-action:none;background:rgba(255,140,40,.42);border:2px solid rgba(255,255,255,.62);color:#fff}
#v2mine.on{display:flex}
#v2mine:active{background:rgba(255,140,40,.78)}
/* Тач-устройства: джойстик виден. Десктоп: скрыт навсегда — работает клавиатура. */
@media (hover:none) and (pointer:coarse){ #v2joy.on{display:block} #v2jump.on,#v2mine.on{display:flex} }
@media (hover:hover) and (pointer:fine){ #v2joy,#v2jump,#v2mine{display:none !important} }
</style>
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats" title="Тяни по экрану — поворот камеры · колесо/щипок — зум · тап по блоку — добыча/стройка">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">🌡️ <span id="temp">15°C</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row">⭐ Lv.<span id="xplevel">1</span> | XP: <span id="xpbar">0/50</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="menuBtn" class="rbtn pe">⋯</div>
<div id="menuDropdown" class="menu-dropdown">
<div id="saveItem" class="menu-item">💾 Сохранить</div>
<div id="resetItem" class="menu-item">🔄 Новый мир</div>
<div id="marketItem" class="menu-item">🏪 Рынок</div>
<div id="settingsItem" class="menu-item">⚙️ Настройки</div>
</div>
<div id="selectedItem"></div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:190px;top:10px;z-index:200;pointer-events:auto;">
<canvas id="minimap" width="200" height="120" style="border:2px solid rgba(255,255,255,0.7);border-radius:8px;background:rgba(0,0,0,0.8);"></canvas>
<span id="minimapClose" style="position:absolute;top:2px;right:6px;cursor:pointer;color:#fff;font-size:18px;font-weight:900;z-index:201;">✕</span>
</div>
<!-- Печь -->
<div id="furnacePanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🔥 Печь</span>
<span id="furnaceClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="furnaceContent" style="padding:8px;"></div>
</div>
<!-- Джойстик вместо стрелок: движение во все стороны по горизонтали -->
<div id="v2joy"><div class="knob"></div></div>
<div id="v2jump">⬆️</div>
<div id="v2mine">⛏️</div>
<div id="craftPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Крафт</span>
<span id="craftClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="recipes"></div>
</div>
<div id="inventoryPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Инвентарь</span>
<span id="inventoryClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="inventoryGrid"></div>
</div>
<div id="chatPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Чат</span>
<span id="chatClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="chatMessages"></div>
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Введите сообщение...">
<button id="chatSend">Отправить</button>
</div>
</div>
<div id="death" class="death-screen" style="display:none;">
<div class="death-content">
<h1>💀 Вы погибли!</h1>
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
</div>
</div>
<div id="marketPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🏪 Рынок</span>
<span id="marketClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="marketContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
</div>
<div id="settingsPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>⚙️ Настройки</span>
<span id="settingsClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="settingsContent" style="padding:8px;max-height:60vh;overflow-y:auto;">
<label style="display:flex;align-items:center;gap:8px;color:#fff"><input type="checkbox" id="dayNightToggle"> Цикл день/ночь</label>
<div style="margin-top:10px;color:#fff;">
<label style="font-size:13px;">Время суток</label>
<input type="range" id="timeSlider" min="0" max="100" value="0" style="width:100%;accent-color:#f1c40f;">
<div style="display:flex;justify-content:space-between;font-size:10px;color:#aaa;">
<span>Рассвет</span><span>День</span><span>Закат</span><span>Ночь</span>
</div>
</div>
<div style="margin-top:12px;color:#fff;">
<label style="font-size:13px;">Погода</label>
<div style="display:flex;gap:6px;margin-top:4px;">
<button id="weatherClear" class="weather-btn weather-active" data-weather="clear" style="flex:1;padding:6px;background:#3498db;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Ясно</button>
<button id="weatherRain" class="weather-btn" data-weather="rain" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Дождь</button>
<button id="weatherSnow" class="weather-btn" data-weather="snow" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Снег</button>
<button id="weatherThunder" class="weather-btn" data-weather="thunder" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Гроза</button>
</div>
<label style="font-size:12px;color:#aaa;margin-top:6px;display:block;">Интенсивность</label>
<input type="range" id="weatherIntensity" min="0" max="100" value="40" style="width:100%;accent-color:#3498db;">
</div>
</div>
</div>
<div id="sleepOverlay" style="display:none;position:absolute;inset:0;z-index:9;align-items:center;justify-content:center;flex-direction:column;background:rgba(0,0,0,0.7);color:#fff;font:700 32px system-ui;">💤 Спим...<div style="font:400 18px system-ui;margin-top:10px;">Нажмите на кровать чтобы проснуться</div>
</div>
</div>
<script src="three147.min.js"></script>
<script src="game3d.js?v=4"></script>
</body>
</html>

View File

@ -2,13 +2,61 @@
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>GrechkaCraft: Multiplayer</title>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=6">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#1a1a2e" />
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="imgs/icon-192.png" />
<title>GrechkaCraft</title>
<!-- Telegram WebApp: УБРАН (v80). Синхронный скрипт telegram.org вешал парсер:
если сеть до него тупила, страница оставалась белой десятки секунд.
Весь TG-код за гардами (isTgWebApp), без скрипта игра работает как обычный веб. -->
<!-- Socket.io: грузится в конце body (v81), а НЕ в head — синхронный скрипт
в head блокировал парсер: заставка #boot не рисовалась, пока CDN не ответит.
Порядок сохранён: socket.io идёт строго перед game.js. -->
<link rel="stylesheet" href="style.css?v=7">
</head>
<body>
<!-- BOOT v79: мгновенная заставка. Рисуется парсером ДО внешних скриптов и game.js,
поэтому белый экран исключён. Убирается из игры вызовом hideBoot()
(меню / авто-join / заглушка генерации). Встроенный таймер не зависит от
game.js: если игра не стартовала за 20 с — показывает кнопку «Обновить». -->
<div id="boot" data-v="79">
<style>
#boot{position:fixed;inset:0;z-index:2147482999;display:flex;align-items:center;justify-content:center;background:linear-gradient(#0f1720,#1c2b3a);font:14px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;color:#e8f0f8}
.boot-box{text-align:center;max-width:320px;padding:0 24px}
.boot-logo{width:60px;height:60px;margin:0 auto 12px;border-radius:14px;background:#5a8f3c;position:relative}
.boot-logo:before{content:"";position:absolute;left:12px;right:12px;top:16px;height:10px;background:#7cc24a;border-radius:5px}
.boot-logo:after{content:"";position:absolute;left:18px;right:18px;top:34px;height:8px;background:#e8f0f8;border-radius:4px}
.boot-title{font-size:22px;margin-bottom:2px}
.boot-ver{color:#8fa3b8;font-size:12px;margin-bottom:14px}
.boot-bar{height:6px;background:rgba(255,255,255,.12);border-radius:3px;overflow:hidden}
.boot-bar>i{display:block;height:100%;width:35%;background:#5a8f3c;border-radius:3px;animation:bootSlide 1.1s ease-in-out infinite}
@keyframes bootSlide{0%{margin-left:-35%}100%{margin-left:100%}}
.boot-hint{margin-top:12px;color:#8fa3b8;font-size:12px;min-height:36px}
.boot-btn{display:none;font:inherit;margin-top:10px;padding:11px 22px;border:0;border-radius:9px;background:#5a8f3c;color:#fff;cursor:pointer}
</style>
<div class="boot-box">
<div class="boot-logo"></div>
<div class="boot-title">Гречка</div>
<div class="boot-ver">v79</div>
<div class="boot-bar"><i></i></div>
<div class="boot-hint" id="bootHint">Загрузка игры…</div>
<button class="boot-btn" id="bootRetry" onclick="location.reload()">Обновить</button>
</div>
<script>
setTimeout(function(){
var boot=document.getElementById('boot');
if(boot&&boot.isConnected){
var h=document.getElementById('bootHint'),b=document.getElementById('bootRetry');
if(h)h.textContent='Загрузка затянулась — проверь интернет и нажми «Обновить».';
if(b)b.style.display='inline-block';
}
},20000);
</script>
</div>
<div id="game">
<canvas id="c"></canvas>
@ -17,6 +65,7 @@
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">🌡️ <span id="temp">15°C</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
@ -25,19 +74,26 @@
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="saveBtn" class="rbtn pe">💾</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="resetBtn" class="rbtn pe">🔄</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="menuBtn" class="rbtn pe">⋯</div>
<div id="menuDropdown" class="menu-dropdown">
<div id="saveItem" class="menu-item">💾 Сохранить</div>
<div id="resetItem" class="menu-item">🔄 Новый мир</div>
<div id="marketItem" class="menu-item">🏪 Рынок</div>
<div id="settingsItem" class="menu-item">⚙️ Настройки</div>
</div>
<div id="selectedItem"></div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:190px;top:10px;z-index:200;pointer-events:auto;">
<canvas id="minimap" width="200" height="120" style="border:2px solid rgba(255,255,255,0.7);border-radius:8px;background:rgba(0,0,0,0.8);"></canvas>
<span id="minimapClose" style="position:absolute;top:2px;right:6px;cursor:pointer;color:#fff;font-size:18px;font-weight:900;z-index:201;">✕</span>
</div>
<!-- Печь -->
@ -91,8 +147,45 @@
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
</div>
</div>
<div id="marketPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🏪 Рынок</span>
<span id="marketClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="marketContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
</div>
<div id="settingsPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>⚙️ Настройки</span>
<span id="settingsClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="settingsContent" style="padding:8px;max-height:60vh;overflow-y:auto;">
<label style="display:flex;align-items:center;gap:8px;color:#fff"><input type="checkbox" id="dayNightToggle"> Цикл день/ночь</label>
<div style="margin-top:10px;color:#fff;">
<label style="font-size:13px;">Время суток</label>
<input type="range" id="timeSlider" min="0" max="100" value="0" style="width:100%;accent-color:#f1c40f;">
<div style="display:flex;justify-content:space-between;font-size:10px;color:#aaa;">
<span>Рассвет</span><span>День</span><span>Закат</span><span>Ночь</span>
</div>
</div>
<div style="margin-top:12px;color:#fff;">
<label style="font-size:13px;">Погода</label>
<div style="display:flex;gap:6px;margin-top:4px;">
<button id="weatherClear" class="weather-btn weather-active" data-weather="clear" style="flex:1;padding:6px;background:#3498db;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Ясно</button>
<button id="weatherRain" class="weather-btn" data-weather="rain" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Дождь</button>
<button id="weatherSnow" class="weather-btn" data-weather="snow" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Снег</button>
<button id="weatherThunder" class="weather-btn" data-weather="thunder" style="flex:1;padding:6px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:12px;">Гроза</button>
</div>
<label style="font-size:12px;color:#aaa;margin-top:6px;display:block;">Интенсивность</label>
<input type="range" id="weatherIntensity" min="0" max="100" value="40" style="width:100%;accent-color:#3498db;">
</div>
</div>
</div>
</div>
<script src="game.js?v=24"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script src="game.js?v=79"></script>
</body>
</html>

110
index.html.bak.v30 Normal file
View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>GrechkaCraft: Multiplayer</title>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=10">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row">⭐ Lv.<span id="xplevel">1</span> | XP: <span id="xpbar">0/50</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="menuBtn" class="rbtn pe">⋯</div>
<div id="menuDropdown" class="menu-dropdown">
<div id="saveItem" class="menu-item">💾 Сохранить</div>
<div id="resetItem" class="menu-item">🔄 Новый мир</div>
<div id="marketItem" class="menu-item">🏪 Рынок</div>
</div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:190px;top:10px;z-index:200;pointer-events:auto;">
<canvas id="minimap" width="200" height="120" style="border:2px solid rgba(255,255,255,0.7);border-radius:8px;background:rgba(0,0,0,0.8);"></canvas>
</div>
<!-- Печь -->
<div id="furnacePanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🔥 Печь</span>
<span id="furnaceClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="furnaceContent" style="padding:8px;"></div>
</div>
<div id="controls">
<div id="left" class="btn pe">⬅️</div>
<div id="jump" class="btn pe">⬆️</div>
<div id="down" class="btn pe">⬇️</div>
<div id="right" class="btn pe">➡️</div>
</div>
<div id="craftPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Крафт</span>
<span id="craftClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="recipes"></div>
</div>
<div id="inventoryPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Инвентарь</span>
<span id="inventoryClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="inventoryGrid"></div>
</div>
<div id="chatPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Чат</span>
<span id="chatClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="chatMessages"></div>
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Введите сообщение...">
<button id="chatSend">Отправить</button>
</div>
</div>
<div id="death" class="death-screen" style="display:none;">
<div class="death-content">
<h1>💀 Вы погибли!</h1>
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
</div>
</div>
<div id="marketPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🏪 Рынок</span>
<span id="marketClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="marketContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
</div>
</div>
<script src="game.js?v=30"></script>
</body>
</html>

125
index.html_new Normal file
View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#1a1a2e" />
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="imgs/grechka_1.png" />
<title>GrechkaCraft</title>
<!-- Socket.io Client -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=6">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row">⭐ Lv.<span id="xplevel">1</span> | XP: <span id="xpbar">0/50</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="menuBtn" class="rbtn pe">⋯</div>
<div id="menuDropdown" class="menu-dropdown">
<div id="saveItem" class="menu-item">💾 Сохранить</div>
<div id="resetItem" class="menu-item">🔄 Новый мир</div>
<div id="marketItem" class="menu-item">🏪 Рынок</div>
<div id="settingsItem" class="menu-item">⚙️ Настройки</div>
</div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:190px;top:10px;z-index:200;pointer-events:auto;">
<canvas id="minimap" width="200" height="120" style="border:2px solid rgba(255,255,255,0.7);border-radius:8px;background:rgba(0,0,0,0.8);"></canvas>
</div>
<!-- Печь -->
<div id="furnacePanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🔥 Печь</span>
<span id="furnaceClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="furnaceContent" style="padding:8px;"></div>
</div>
<div id="controls">
<div id="left" class="btn pe">⬅️</div>
<div id="jump" class="btn pe">⬆️</div>
<div id="down" class="btn pe">⬇️</div>
<div id="right" class="btn pe">➡️</div>
</div>
<div id="craftPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Крафт</span>
<span id="craftClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="recipes"></div>
</div>
<div id="inventoryPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Инвентарь</span>
<span id="inventoryClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="inventoryGrid"></div>
</div>
<div id="chatPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>Чат</span>
<span id="chatClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="chatMessages"></div>
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Введите сообщение...">
<button id="chatSend">Отправить</button>
</div>
</div>
<div id="death" class="death-screen" style="display:none;">
<div class="death-content">
<h1>💀 Вы погибли!</h1>
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
</div>
</div>
<div id="marketPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>🏪 Рынок</span>
<span id="marketClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="marketContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
</div>
<div id="settingsPanel" class="panel" style="display:none;">
<div class="panel-header">
<span>⚙️ Настройки</span>
<span id="settingsClose" class="close" style="cursor:pointer;">✕</span>
</div>
<div id="settingsContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
</div>
</div>
<script src="game.js?v=72"></script>
</body>
</html>

19
manifest.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "GrechkaCraft",
"short_name": "GrechkaCraft",
"description": "Voxel игра в браузере",
"start_url": "/?v=73",
"display": "fullscreen",
"orientation": "any",
"background_color": "#111111",
"theme_color": "#1a1a2e",
"categories": ["games"],
"icons": [
{
"src": "imgs/grechka_1.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}

View File

@ -3,9 +3,17 @@ server {
root /usr/share/nginx/html;
index index.html;
# CORS for ES modules
add_header Access-Control-Allow-Origin *;
location = /v2 {
rewrite ^ /v2.html last;
}
location = /v2.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Access-Control-Allow-Origin *;
}
location ~* \.(js|mjs|css)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
@ -14,6 +22,14 @@ server {
default_type application/javascript;
}
location /src/ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Access-Control-Allow-Origin *;
add_header Content-Type "application/javascript; charset=utf-8";
expires 0;
}
location / {
try_files $uri $uri/ /index.html;
}

1
node_modules/.bin/esbuild generated vendored Symbolic link
View File

@ -0,0 +1 @@
../esbuild/bin/esbuild

64
node_modules/.package-lock.json generated vendored Normal file
View File

@ -0,0 +1,64 @@
{
"name": "grechka-game",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
}
}
}

3
node_modules/@esbuild/linux-x64/README.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.

BIN
node_modules/@esbuild/linux-x64/bin/esbuild generated vendored Executable file

Binary file not shown.

20
node_modules/@esbuild/linux-x64/package.json generated vendored Normal file
View File

@ -0,0 +1,20 @@
{
"name": "@esbuild/linux-x64",
"version": "0.28.0",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}

21
node_modules/esbuild/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
node_modules/esbuild/README.md generated vendored Normal file
View File

@ -0,0 +1,3 @@
# esbuild
This is a JavaScript bundler and minifier. See https://github.com/evanw/esbuild and the [JavaScript API documentation](https://esbuild.github.io/api/) for details.

BIN
node_modules/esbuild/bin/esbuild generated vendored Executable file

Binary file not shown.

300
node_modules/esbuild/install.js generated vendored Normal file
View File

@ -0,0 +1,300 @@
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// lib/npm/node-platform.ts
var fs = require("fs");
var os = require("os");
var path = require("path");
var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH;
var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild";
var knownWindowsPackages = {
"win32 arm64 LE": "@esbuild/win32-arm64",
"win32 ia32 LE": "@esbuild/win32-ia32",
"win32 x64 LE": "@esbuild/win32-x64"
};
var knownUnixlikePackages = {
"aix ppc64 BE": "@esbuild/aix-ppc64",
"android arm64 LE": "@esbuild/android-arm64",
"darwin arm64 LE": "@esbuild/darwin-arm64",
"darwin x64 LE": "@esbuild/darwin-x64",
"freebsd arm64 LE": "@esbuild/freebsd-arm64",
"freebsd x64 LE": "@esbuild/freebsd-x64",
"linux arm LE": "@esbuild/linux-arm",
"linux arm64 LE": "@esbuild/linux-arm64",
"linux ia32 LE": "@esbuild/linux-ia32",
"linux mips64el LE": "@esbuild/linux-mips64el",
"linux ppc64 LE": "@esbuild/linux-ppc64",
"linux riscv64 LE": "@esbuild/linux-riscv64",
"linux s390x BE": "@esbuild/linux-s390x",
"linux x64 LE": "@esbuild/linux-x64",
"linux loong64 LE": "@esbuild/linux-loong64",
"netbsd arm64 LE": "@esbuild/netbsd-arm64",
"netbsd x64 LE": "@esbuild/netbsd-x64",
"openbsd arm64 LE": "@esbuild/openbsd-arm64",
"openbsd x64 LE": "@esbuild/openbsd-x64",
"sunos x64 LE": "@esbuild/sunos-x64"
};
var knownWebAssemblyFallbackPackages = {
"android arm LE": "@esbuild/android-arm",
"android x64 LE": "@esbuild/android-x64",
"openharmony arm64 LE": "@esbuild/openharmony-arm64"
};
function pkgAndSubpathForCurrentPlatform() {
let pkg;
let subpath;
let isWASM = false;
let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`;
if (platformKey in knownWindowsPackages) {
pkg = knownWindowsPackages[platformKey];
subpath = "esbuild.exe";
} else if (platformKey in knownUnixlikePackages) {
pkg = knownUnixlikePackages[platformKey];
subpath = "bin/esbuild";
} else if (platformKey in knownWebAssemblyFallbackPackages) {
pkg = knownWebAssemblyFallbackPackages[platformKey];
subpath = "bin/esbuild";
isWASM = true;
} else {
throw new Error(`Unsupported platform: ${platformKey}`);
}
return { pkg, subpath, isWASM };
}
function downloadedBinPath(pkg, subpath) {
const esbuildLibDir = path.dirname(require.resolve("esbuild"));
return path.join(esbuildLibDir, `downloaded-${pkg.replace("/", "-")}-${path.basename(subpath)}`);
}
// lib/npm/node-install.ts
var fs2 = require("fs");
var os2 = require("os");
var path2 = require("path");
var zlib = require("zlib");
var https = require("https");
var crypto = require("crypto");
var child_process = require("child_process");
var packageJSON = require(path2.join(__dirname, "package.json"));
var toPath = path2.join(__dirname, "bin", "esbuild");
var isToPathJS = true;
function validateBinaryVersion(...command) {
command.push("--version");
let stdout;
try {
stdout = child_process.execFileSync(command.shift(), command, {
// Without this, this install script strangely crashes with the error
// "EACCES: permission denied, write" but only on Ubuntu Linux when node is
// installed from the Snap Store. This is not a problem when you download
// the official version of node. The problem appears to be that stderr
// (i.e. file descriptor 2) isn't writable?
//
// More info:
// - https://snapcraft.io/ (what the Snap Store is)
// - https://nodejs.org/dist/ (download the official version of node)
// - https://github.com/evanw/esbuild/issues/1711#issuecomment-1027554035
//
stdio: "pipe"
}).toString().trim();
} catch (err) {
if (os2.platform() === "darwin" && /_SecTrustEvaluateWithError/.test(err + "")) {
let os3 = "this version of macOS";
try {
os3 = "macOS " + child_process.execFileSync("sw_vers", ["-productVersion"]).toString().trim();
} catch {
}
throw new Error(`The "esbuild" package cannot be installed because ${os3} is too outdated.
The Go compiler (which esbuild relies on) no longer supports ${os3},
which means the "esbuild" binary executable can't be run. You can either:
* Update your version of macOS to one that the Go compiler supports
* Use the "esbuild-wasm" package instead of the "esbuild" package
* Build esbuild yourself using an older version of the Go compiler
`);
}
throw err;
}
if (stdout !== packageJSON.version) {
throw new Error(`Expected ${JSON.stringify(packageJSON.version)} but got ${JSON.stringify(stdout)}`);
}
}
function isYarn() {
const { npm_config_user_agent } = process.env;
if (npm_config_user_agent) {
return /\byarn\//.test(npm_config_user_agent);
}
return false;
}
function fetch(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location)
return fetch(res.headers.location).then(resolve, reject);
if (res.statusCode !== 200)
return reject(new Error(`Server responded with ${res.statusCode}`));
let chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve(Buffer.concat(chunks)));
}).on("error", reject);
});
}
function extractFileFromTarGzip(buffer, subpath) {
try {
buffer = zlib.unzipSync(buffer);
} catch (err) {
throw new Error(`Invalid gzip data in archive: ${err && err.message || err}`);
}
let str = (i, n) => String.fromCharCode(...buffer.subarray(i, i + n)).replace(/\0.*$/, "");
let offset = 0;
subpath = `package/${subpath}`;
while (offset < buffer.length) {
let name = str(offset, 100);
let size = parseInt(str(offset + 124, 12), 8);
offset += 512;
if (!isNaN(size)) {
if (name === subpath) return buffer.subarray(offset, offset + size);
offset += size + 511 & ~511;
}
}
throw new Error(`Could not find ${JSON.stringify(subpath)} in archive`);
}
function installUsingNPM(pkg, subpath, binPath) {
const env = { ...process.env, npm_config_global: void 0 };
const esbuildLibDir = path2.dirname(require.resolve("esbuild"));
const installDir = path2.join(esbuildLibDir, "npm-install");
fs2.mkdirSync(installDir);
try {
fs2.writeFileSync(path2.join(installDir, "package.json"), "{}");
child_process.execSync(
`npm install --loglevel=error --prefer-offline --no-audit --progress=false ${pkg}@${packageJSON.version}`,
{ cwd: installDir, stdio: "pipe", env }
);
const installedBinPath = path2.join(installDir, "node_modules", pkg, subpath);
binaryIntegrityCheck(pkg, subpath, fs2.readFileSync(installedBinPath));
fs2.renameSync(installedBinPath, binPath);
} finally {
try {
removeRecursive(installDir);
} catch {
}
}
}
function removeRecursive(dir) {
for (const entry of fs2.readdirSync(dir)) {
const entryPath = path2.join(dir, entry);
let stats;
try {
stats = fs2.lstatSync(entryPath);
} catch {
continue;
}
if (stats.isDirectory()) removeRecursive(entryPath);
else fs2.unlinkSync(entryPath);
}
fs2.rmdirSync(dir);
}
function applyManualBinaryPathOverride(overridePath) {
const pathString = JSON.stringify(overridePath);
fs2.writeFileSync(toPath, `#!/usr/bin/env node
require('child_process').execFileSync(${pathString}, process.argv.slice(2), { stdio: 'inherit' });
`);
const libMain = path2.join(__dirname, "lib", "main.js");
const code = fs2.readFileSync(libMain, "utf8");
fs2.writeFileSync(libMain, `var ESBUILD_BINARY_PATH = ${pathString};
${code}`);
}
function maybeOptimizePackage(binPath, isWASM) {
if (os2.platform() !== "win32" && !isYarn() && !isWASM) {
const tempPath = path2.join(__dirname, "bin-esbuild");
try {
fs2.linkSync(binPath, tempPath);
fs2.renameSync(tempPath, toPath);
isToPathJS = false;
fs2.unlinkSync(tempPath);
} catch {
}
}
}
function binaryIntegrityCheck(pkg, subpath, bytes) {
const hash = crypto.createHash("sha256").update(bytes).digest("hex");
const key = `${pkg}/${subpath}`;
const expected = packageJSON["esbuild.binaryHashes"][key];
if (!expected) throw new Error(`Missing hash for "${key}"`);
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..."`);
}
async function downloadDirectlyFromNPM(pkg, subpath, binPath) {
const url = `https://registry.npmjs.org/${pkg}/-/${pkg.replace("@esbuild/", "")}-${packageJSON.version}.tgz`;
console.error(`[esbuild] Trying to download ${JSON.stringify(url)}`);
try {
const bytes = extractFileFromTarGzip(await fetch(url), subpath);
binaryIntegrityCheck(pkg, subpath, bytes);
fs2.writeFileSync(binPath, bytes);
fs2.chmodSync(binPath, 493);
} catch (e) {
console.error(`[esbuild] Failed to download ${JSON.stringify(url)}: ${e && e.message || e}`);
throw e;
}
}
async function checkAndPreparePackage() {
if (isValidBinaryPath(ESBUILD_BINARY_PATH)) {
if (!fs2.existsSync(ESBUILD_BINARY_PATH)) {
console.warn(`[esbuild] Ignoring bad configuration: ESBUILD_BINARY_PATH=${ESBUILD_BINARY_PATH}`);
} else {
applyManualBinaryPathOverride(ESBUILD_BINARY_PATH);
return;
}
}
const { pkg, subpath, isWASM } = pkgAndSubpathForCurrentPlatform();
let binPath;
try {
binPath = require.resolve(`${pkg}/${subpath}`);
} catch (e) {
console.error(`[esbuild] Failed to find package "${pkg}" on the file system
This can happen if you use the "--no-optional" flag. The "optionalDependencies"
package.json feature is used by esbuild to install the correct binary executable
for your current platform. This install script will now attempt to work around
this. If that fails, you need to remove the "--no-optional" flag to use esbuild.
`);
if (isWASM) throw new Error(`Failed to install package "${pkg}"`);
binPath = downloadedBinPath(pkg, subpath);
try {
console.error(`[esbuild] Trying to install package "${pkg}" using npm`);
installUsingNPM(pkg, subpath, binPath);
} catch (e2) {
console.error(`[esbuild] Failed to install package "${pkg}" using npm: ${e2 && e2.message || e2}`);
try {
await downloadDirectlyFromNPM(pkg, subpath, binPath);
} catch (e3) {
throw new Error(`Failed to install package "${pkg}"`);
}
}
}
maybeOptimizePackage(binPath, isWASM);
}
checkAndPreparePackage().then(() => {
if (isToPathJS) {
validateBinaryVersion(process.execPath, toPath);
} else {
validateBinaryVersion(toPath);
}
});

716
node_modules/esbuild/lib/main.d.ts generated vendored Normal file
View File

@ -0,0 +1,716 @@
export type Platform = 'browser' | 'node' | 'neutral'
export type Format = 'iife' | 'cjs' | 'esm'
export type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx'
export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent'
export type Charset = 'ascii' | 'utf8'
export type Drop = 'console' | 'debugger'
export type AbsPaths = 'code' | 'log' | 'metafile'
interface CommonOptions {
/** Documentation: https://esbuild.github.io/api/#sourcemap */
sourcemap?: boolean | 'linked' | 'inline' | 'external' | 'both'
/** Documentation: https://esbuild.github.io/api/#legal-comments */
legalComments?: 'none' | 'inline' | 'eof' | 'linked' | 'external'
/** Documentation: https://esbuild.github.io/api/#source-root */
sourceRoot?: string
/** Documentation: https://esbuild.github.io/api/#sources-content */
sourcesContent?: boolean
/** Documentation: https://esbuild.github.io/api/#format */
format?: Format
/** Documentation: https://esbuild.github.io/api/#global-name */
globalName?: string
/** Documentation: https://esbuild.github.io/api/#target */
target?: string | string[]
/** Documentation: https://esbuild.github.io/api/#supported */
supported?: Record<string, boolean>
/** Documentation: https://esbuild.github.io/api/#platform */
platform?: Platform
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleProps?: RegExp
/** Documentation: https://esbuild.github.io/api/#mangle-props */
reserveProps?: RegExp
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleQuoted?: boolean
/** Documentation: https://esbuild.github.io/api/#mangle-props */
mangleCache?: Record<string, string | false>
/** Documentation: https://esbuild.github.io/api/#drop */
drop?: Drop[]
/** Documentation: https://esbuild.github.io/api/#drop-labels */
dropLabels?: string[]
/** Documentation: https://esbuild.github.io/api/#minify */
minify?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifyWhitespace?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifyIdentifiers?: boolean
/** Documentation: https://esbuild.github.io/api/#minify */
minifySyntax?: boolean
/** Documentation: https://esbuild.github.io/api/#line-limit */
lineLimit?: number
/** Documentation: https://esbuild.github.io/api/#charset */
charset?: Charset
/** Documentation: https://esbuild.github.io/api/#tree-shaking */
treeShaking?: boolean
/** Documentation: https://esbuild.github.io/api/#ignore-annotations */
ignoreAnnotations?: boolean
/** Documentation: https://esbuild.github.io/api/#jsx */
jsx?: 'transform' | 'preserve' | 'automatic'
/** Documentation: https://esbuild.github.io/api/#jsx-factory */
jsxFactory?: string
/** Documentation: https://esbuild.github.io/api/#jsx-fragment */
jsxFragment?: string
/** Documentation: https://esbuild.github.io/api/#jsx-import-source */
jsxImportSource?: string
/** Documentation: https://esbuild.github.io/api/#jsx-development */
jsxDev?: boolean
/** Documentation: https://esbuild.github.io/api/#jsx-side-effects */
jsxSideEffects?: boolean
/** Documentation: https://esbuild.github.io/api/#define */
define?: { [key: string]: string }
/** Documentation: https://esbuild.github.io/api/#pure */
pure?: string[]
/** Documentation: https://esbuild.github.io/api/#keep-names */
keepNames?: boolean
/** Documentation: https://esbuild.github.io/api/#abs-paths */
absPaths?: AbsPaths[]
/** Documentation: https://esbuild.github.io/api/#color */
color?: boolean
/** Documentation: https://esbuild.github.io/api/#log-level */
logLevel?: LogLevel
/** Documentation: https://esbuild.github.io/api/#log-limit */
logLimit?: number
/** Documentation: https://esbuild.github.io/api/#log-override */
logOverride?: Record<string, LogLevel>
/** Documentation: https://esbuild.github.io/api/#tsconfig-raw */
tsconfigRaw?: string | TsconfigRaw
}
export interface TsconfigRaw {
compilerOptions?: {
alwaysStrict?: boolean
baseUrl?: string
experimentalDecorators?: boolean
importsNotUsedAsValues?: 'remove' | 'preserve' | 'error'
jsx?: 'preserve' | 'react-native' | 'react' | 'react-jsx' | 'react-jsxdev'
jsxFactory?: string
jsxFragmentFactory?: string
jsxImportSource?: string
paths?: Record<string, string[]>
preserveValueImports?: boolean
strict?: boolean
target?: string
useDefineForClassFields?: boolean
verbatimModuleSyntax?: boolean
}
}
export interface BuildOptions extends CommonOptions {
/** Documentation: https://esbuild.github.io/api/#bundle */
bundle?: boolean
/** Documentation: https://esbuild.github.io/api/#splitting */
splitting?: boolean
/** Documentation: https://esbuild.github.io/api/#preserve-symlinks */
preserveSymlinks?: boolean
/** Documentation: https://esbuild.github.io/api/#outfile */
outfile?: string
/** Documentation: https://esbuild.github.io/api/#metafile */
metafile?: boolean
/** Documentation: https://esbuild.github.io/api/#outdir */
outdir?: string
/** Documentation: https://esbuild.github.io/api/#outbase */
outbase?: string
/** Documentation: https://esbuild.github.io/api/#external */
external?: string[]
/** Documentation: https://esbuild.github.io/api/#packages */
packages?: 'bundle' | 'external'
/** Documentation: https://esbuild.github.io/api/#alias */
alias?: Record<string, string>
/** Documentation: https://esbuild.github.io/api/#loader */
loader?: { [ext: string]: Loader }
/** Documentation: https://esbuild.github.io/api/#resolve-extensions */
resolveExtensions?: string[]
/** Documentation: https://esbuild.github.io/api/#main-fields */
mainFields?: string[]
/** Documentation: https://esbuild.github.io/api/#conditions */
conditions?: string[]
/** Documentation: https://esbuild.github.io/api/#write */
write?: boolean
/** Documentation: https://esbuild.github.io/api/#allow-overwrite */
allowOverwrite?: boolean
/** Documentation: https://esbuild.github.io/api/#tsconfig */
tsconfig?: string
/** Documentation: https://esbuild.github.io/api/#out-extension */
outExtension?: { [ext: string]: string }
/** Documentation: https://esbuild.github.io/api/#public-path */
publicPath?: string
/** Documentation: https://esbuild.github.io/api/#entry-names */
entryNames?: string
/** Documentation: https://esbuild.github.io/api/#chunk-names */
chunkNames?: string
/** Documentation: https://esbuild.github.io/api/#asset-names */
assetNames?: string
/** Documentation: https://esbuild.github.io/api/#inject */
inject?: string[]
/** Documentation: https://esbuild.github.io/api/#banner */
banner?: { [type: string]: string }
/** Documentation: https://esbuild.github.io/api/#footer */
footer?: { [type: string]: string }
/** Documentation: https://esbuild.github.io/api/#entry-points */
entryPoints?: (string | { in: string, out: string })[] | Record<string, string>
/** Documentation: https://esbuild.github.io/api/#stdin */
stdin?: StdinOptions
/** Documentation: https://esbuild.github.io/plugins/ */
plugins?: Plugin[]
/** Documentation: https://esbuild.github.io/api/#working-directory */
absWorkingDir?: string
/** Documentation: https://esbuild.github.io/api/#node-paths */
nodePaths?: string[]; // The "NODE_PATH" variable from Node.js
}
export interface StdinOptions {
contents: string | Uint8Array
resolveDir?: string
sourcefile?: string
loader?: Loader
}
export interface Message {
id: string
pluginName: string
text: string
location: Location | null
notes: Note[]
/**
* Optional user-specified data that is passed through unmodified. You can
* use this to stash the original error, for example.
*/
detail: any
}
export interface Note {
text: string
location: Location | null
}
export interface Location {
file: string
namespace: string
/** 1-based */
line: number
/** 0-based, in bytes */
column: number
/** in bytes */
length: number
lineText: string
suggestion: string
}
export interface OutputFile {
path: string
contents: Uint8Array
hash: string
/** "contents" as text (changes automatically with "contents") */
readonly text: string
}
export interface BuildResult<ProvidedOptions extends BuildOptions = BuildOptions> {
errors: Message[]
warnings: Message[]
/** Only when "write: false" */
outputFiles: OutputFile[] | (ProvidedOptions['write'] extends false ? never : undefined)
/** Only when "metafile: true" */
metafile: Metafile | (ProvidedOptions['metafile'] extends true ? never : undefined)
/** Only when "mangleCache" is present */
mangleCache: Record<string, string | false> | (ProvidedOptions['mangleCache'] extends Object ? never : undefined)
}
export interface BuildFailure extends Error {
errors: Message[]
warnings: Message[]
}
/** Documentation: https://esbuild.github.io/api/#serve-arguments */
export interface ServeOptions {
port?: number
host?: string
servedir?: string
keyfile?: string
certfile?: string
fallback?: string
cors?: CORSOptions
onRequest?: (args: ServeOnRequestArgs) => void
}
/** Documentation: https://esbuild.github.io/api/#cors */
export interface CORSOptions {
origin?: string | string[]
}
export interface ServeOnRequestArgs {
remoteAddress: string
method: string
path: string
status: number
/** The time to generate the response, not to send it */
timeInMS: number
}
/** Documentation: https://esbuild.github.io/api/#serve-return-values */
export interface ServeResult {
port: number
hosts: string[]
}
export interface TransformOptions extends CommonOptions {
/** Documentation: https://esbuild.github.io/api/#sourcefile */
sourcefile?: string
/** Documentation: https://esbuild.github.io/api/#loader */
loader?: Loader
/** Documentation: https://esbuild.github.io/api/#banner */
banner?: string
/** Documentation: https://esbuild.github.io/api/#footer */
footer?: string
}
export interface TransformResult<ProvidedOptions extends TransformOptions = TransformOptions> {
code: string
map: string
warnings: Message[]
/** Only when "mangleCache" is present */
mangleCache: Record<string, string | false> | (ProvidedOptions['mangleCache'] extends Object ? never : undefined)
/** Only when "legalComments" is "external" */
legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined)
}
export interface TransformFailure extends Error {
errors: Message[]
warnings: Message[]
}
export interface Plugin {
name: string
setup: (build: PluginBuild) => (void | Promise<void>)
}
export interface PluginBuild {
/** Documentation: https://esbuild.github.io/plugins/#build-options */
initialOptions: BuildOptions
/** Documentation: https://esbuild.github.io/plugins/#resolve */
resolve(path: string, options?: ResolveOptions): Promise<ResolveResult>
/** Documentation: https://esbuild.github.io/plugins/#on-start */
onStart(callback: () =>
(OnStartResult | null | void | Promise<OnStartResult | null | void>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-end */
onEnd(callback: (result: BuildResult) =>
(OnEndResult | null | void | Promise<OnEndResult | null | void>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-resolve */
onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) =>
(OnResolveResult | null | undefined | Promise<OnResolveResult | null | undefined>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-load */
onLoad(options: OnLoadOptions, callback: (args: OnLoadArgs) =>
(OnLoadResult | null | undefined | Promise<OnLoadResult | null | undefined>)): void
/** Documentation: https://esbuild.github.io/plugins/#on-dispose */
onDispose(callback: () => void): void
// This is a full copy of the esbuild library in case you need it
esbuild: {
context: typeof context,
build: typeof build,
buildSync: typeof buildSync,
transform: typeof transform,
transformSync: typeof transformSync,
formatMessages: typeof formatMessages,
formatMessagesSync: typeof formatMessagesSync,
analyzeMetafile: typeof analyzeMetafile,
analyzeMetafileSync: typeof analyzeMetafileSync,
initialize: typeof initialize,
version: typeof version,
}
}
/** Documentation: https://esbuild.github.io/plugins/#resolve-options */
export interface ResolveOptions {
pluginName?: string
importer?: string
namespace?: string
resolveDir?: string
kind?: ImportKind
pluginData?: any
with?: Record<string, string>
}
/** Documentation: https://esbuild.github.io/plugins/#resolve-results */
export interface ResolveResult {
errors: Message[]
warnings: Message[]
path: string
external: boolean
sideEffects: boolean
namespace: string
suffix: string
pluginData: any
}
export interface OnStartResult {
errors?: PartialMessage[]
warnings?: PartialMessage[]
}
export interface OnEndResult {
errors?: PartialMessage[]
warnings?: PartialMessage[]
}
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-options */
export interface OnResolveOptions {
filter: RegExp
namespace?: string
}
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-arguments */
export interface OnResolveArgs {
path: string
importer: string
namespace: string
resolveDir: string
kind: ImportKind
pluginData: any
with: Record<string, string>
}
export type ImportKind =
| 'entry-point'
// JS
| 'import-statement'
| 'require-call'
| 'dynamic-import'
| 'require-resolve'
// CSS
| 'import-rule'
| 'composes-from'
| 'url-token'
/** Documentation: https://esbuild.github.io/plugins/#on-resolve-results */
export interface OnResolveResult {
pluginName?: string
errors?: PartialMessage[]
warnings?: PartialMessage[]
path?: string
external?: boolean
sideEffects?: boolean
namespace?: string
suffix?: string
pluginData?: any
watchFiles?: string[]
watchDirs?: string[]
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-options */
export interface OnLoadOptions {
filter: RegExp
namespace?: string
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-arguments */
export interface OnLoadArgs {
path: string
namespace: string
suffix: string
pluginData: any
with: Record<string, string>
}
/** Documentation: https://esbuild.github.io/plugins/#on-load-results */
export interface OnLoadResult {
pluginName?: string
errors?: PartialMessage[]
warnings?: PartialMessage[]
contents?: string | Uint8Array
resolveDir?: string
loader?: Loader
pluginData?: any
watchFiles?: string[]
watchDirs?: string[]
}
export interface PartialMessage {
id?: string
pluginName?: string
text?: string
location?: Partial<Location> | null
notes?: PartialNote[]
detail?: any
}
export interface PartialNote {
text?: string
location?: Partial<Location> | null
}
/** Documentation: https://esbuild.github.io/api/#metafile */
export interface Metafile {
inputs: {
[path: string]: {
bytes: number
imports: {
path: string
kind: ImportKind
external?: boolean
original?: string
with?: Record<string, string>
}[]
format?: 'cjs' | 'esm'
with?: Record<string, string>
}
}
outputs: {
[path: string]: {
bytes: number
inputs: {
[path: string]: {
bytesInOutput: number
}
}
imports: {
path: string
kind: ImportKind | 'file-loader'
external?: boolean
}[]
exports: string[]
entryPoint?: string
cssBundle?: string
}
}
}
export interface FormatMessagesOptions {
kind: 'error' | 'warning'
color?: boolean
terminalWidth?: number
}
export interface AnalyzeMetafileOptions {
color?: boolean
verbose?: boolean
}
/** Documentation: https://esbuild.github.io/api/#watch-arguments */
export interface WatchOptions {
delay?: number // In milliseconds
}
export interface BuildContext<ProvidedOptions extends BuildOptions = BuildOptions> {
/** Documentation: https://esbuild.github.io/api/#rebuild */
rebuild(): Promise<BuildResult<ProvidedOptions>>
/** Documentation: https://esbuild.github.io/api/#watch */
watch(options?: WatchOptions): Promise<void>
/** Documentation: https://esbuild.github.io/api/#serve */
serve(options?: ServeOptions): Promise<ServeResult>
cancel(): Promise<void>
dispose(): Promise<void>
}
// This is a TypeScript type-level function which replaces any keys in "In"
// that aren't in "Out" with "never". We use this to reject properties with
// typos in object literals. See: https://stackoverflow.com/questions/49580725
type SameShape<Out, In extends Out> = In & { [Key in Exclude<keyof In, keyof Out>]: never }
/**
* This function invokes the "esbuild" command-line tool for you. It returns a
* promise that either resolves with a "BuildResult" object or rejects with a
* "BuildFailure" object.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function build<T extends BuildOptions>(options: SameShape<BuildOptions, T>): Promise<BuildResult<T>>
/**
* This is the advanced long-running form of "build" that supports additional
* features such as watch mode and a local development server.
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function context<T extends BuildOptions>(options: SameShape<BuildOptions, T>): Promise<BuildContext<T>>
/**
* This function transforms a single JavaScript file. It can be used to minify
* JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript
* to older JavaScript. It returns a promise that is either resolved with a
* "TransformResult" object or rejected with a "TransformFailure" object.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#transform
*/
export declare function transform<T extends TransformOptions>(input: string | Uint8Array, options?: SameShape<TransformOptions, T>): Promise<TransformResult<T>>
/**
* Converts log messages to formatted message strings suitable for printing in
* the terminal. This allows you to reuse the built-in behavior of esbuild's
* log message formatter. This is a batch-oriented API for efficiency.
*
* - Works in node: yes
* - Works in browser: yes
*/
export declare function formatMessages(messages: PartialMessage[], options: FormatMessagesOptions): Promise<string[]>
/**
* Pretty-prints an analysis of the metafile JSON to a string. This is just for
* convenience to be able to match esbuild's pretty-printing exactly. If you want
* to customize it, you can just inspect the data in the metafile yourself.
*
* - Works in node: yes
* - Works in browser: yes
*
* Documentation: https://esbuild.github.io/api/#analyze
*/
export declare function analyzeMetafile(metafile: Metafile | string, options?: AnalyzeMetafileOptions): Promise<string>
/**
* A synchronous version of "build".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#build
*/
export declare function buildSync<T extends BuildOptions>(options: SameShape<BuildOptions, T>): BuildResult<T>
/**
* A synchronous version of "transform".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#transform
*/
export declare function transformSync<T extends TransformOptions>(input: string | Uint8Array, options?: SameShape<TransformOptions, T>): TransformResult<T>
/**
* A synchronous version of "formatMessages".
*
* - Works in node: yes
* - Works in browser: no
*/
export declare function formatMessagesSync(messages: PartialMessage[], options: FormatMessagesOptions): string[]
/**
* A synchronous version of "analyzeMetafile".
*
* - Works in node: yes
* - Works in browser: no
*
* Documentation: https://esbuild.github.io/api/#analyze
*/
export declare function analyzeMetafileSync(metafile: Metafile | string, options?: AnalyzeMetafileOptions): string
/**
* This configures the browser-based version of esbuild. It is necessary to
* call this first and wait for the returned promise to be resolved before
* making other API calls when using esbuild in the browser.
*
* - Works in node: yes
* - Works in browser: yes ("options" is required)
*
* Documentation: https://esbuild.github.io/api/#browser
*/
export declare function initialize(options: InitializeOptions): Promise<void>
export interface InitializeOptions {
/**
* The URL of the "esbuild.wasm" file. This must be provided when running
* esbuild in the browser.
*/
wasmURL?: string | URL
/**
* The result of calling "new WebAssembly.Module(buffer)" where "buffer"
* is a typed array or ArrayBuffer containing the binary code of the
* "esbuild.wasm" file.
*
* You can use this as an alternative to "wasmURL" for environments where it's
* not possible to download the WebAssembly module.
*/
wasmModule?: WebAssembly.Module
/**
* By default esbuild runs the WebAssembly-based browser API in a web worker
* to avoid blocking the UI thread. This can be disabled by setting "worker"
* to false.
*/
worker?: boolean
}
export let version: string
// Call this function to terminate esbuild's child process. The child process
// is not terminated and re-created after each API call because it's more
// efficient to keep it around when there are multiple API calls.
//
// In node this happens automatically before the parent node process exits. So
// you only need to call this if you know you will not make any more esbuild
// API calls and you want to clean up resources.
//
// Unlike node, Deno lacks the necessary APIs to clean up child processes
// automatically. You must manually call stop() in Deno when you're done
// using esbuild or Deno will continue running forever.
//
// Another reason you might want to call this is if you are using esbuild from
// within a Deno test. Deno fails tests that create a child process without
// killing it before the test ends, so you have to call this function (and
// await the returned promise) in every Deno test that uses esbuild.
export declare function stop(): Promise<void>
// Note: These declarations exist to avoid type errors when you omit "dom" from
// "lib" in your "tsconfig.json" file. TypeScript confusingly declares the
// global "WebAssembly" type in "lib.dom.d.ts" even though it has nothing to do
// with the browser DOM and is present in many non-browser JavaScript runtimes
// (e.g. node and deno). Declaring it here allows esbuild's API to be used in
// these scenarios.
//
// There's an open issue about getting this problem corrected (although these
// declarations will need to remain even if this is fixed for backward
// compatibility with older TypeScript versions):
//
// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826
//
declare global {
namespace WebAssembly {
interface Module {
}
}
interface URL {
}
}

2532
node_modules/esbuild/lib/main.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

74
node_modules/esbuild/package.json generated vendored Normal file
View File

@ -0,0 +1,74 @@
{
"name": "esbuild",
"version": "0.28.0",
"description": "An extremely fast JavaScript and CSS bundler and minifier.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"scripts": {
"postinstall": "node install.js"
},
"main": "lib/main.js",
"types": "lib/main.d.ts",
"engines": {
"node": ">=18"
},
"bin": {
"esbuild": "bin/esbuild"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
},
"license": "MIT",
"esbuild.binaryHashes": {
"@esbuild/aix-ppc64/bin/esbuild": "2037fe2a315e737da7d8fbfff5a582243e0a2ee32462f08c69d4833b4e3f9e06",
"@esbuild/android-arm64/bin/esbuild": "6acacac48d2104e6e58bd92104e86acfb3c8f344e965a910a749ea024ec044e5",
"@esbuild/darwin-arm64/bin/esbuild": "6f0e1237f63fa3bc03963e58f0b0be1b9bfacd8f2dc9a3f28483e8f97e4ef2d6",
"@esbuild/darwin-x64/bin/esbuild": "23dadf855f4ef8cf7c159ed5e4018b8f95d66f7e230a3a04f0add7fb809640ad",
"@esbuild/freebsd-arm64/bin/esbuild": "94f2b3af237a7fb2c5061f311e35b7fca15553304f77222b4da7996a35049b88",
"@esbuild/freebsd-x64/bin/esbuild": "c9f571484b1661b93c90cca5781fccd8fc08116eccd4c77079452e51aa2a8240",
"@esbuild/linux-arm/bin/esbuild": "cd2cf1814f32e671c3765e9a26da6a28fc659be2f95426e8f4a116a1f4eb6949",
"@esbuild/linux-arm64/bin/esbuild": "f76afb23c4c27f7837a4fc5fc7123e2e2c75c7f8c6a54e09bddfc06d39e288dd",
"@esbuild/linux-ia32/bin/esbuild": "37ce681c3d0193063cbbf95267d191583f2ffbb818fc81716915f99613af42e9",
"@esbuild/linux-loong64/bin/esbuild": "0565b36f66537e657447aacf6a79f0d4fb91bca78803d20e46c9077e75af8230",
"@esbuild/linux-mips64el/bin/esbuild": "33e1bdf0e9423efddb2515b0a29fa409826a18eadd6340fff0e3927574b58475",
"@esbuild/linux-ppc64/bin/esbuild": "38a3dbbce29bb4ffb5fa2121478f46eb0d6d4979687718c20111372ccfdba169",
"@esbuild/linux-riscv64/bin/esbuild": "2a7317332a2d2bc27dc0ad33123571fa1a9f3aa448c07f6eb7b7c11354e49da8",
"@esbuild/linux-s390x/bin/esbuild": "d4eb9dcf9f112fee07d3f2256b397cc1ff49ac097d579d07dc5ceacec1276c12",
"@esbuild/linux-x64/bin/esbuild": "aafacdf135322bf47c882a4ea4db33d0375583f5b9c3fd2d4e12258e470568be",
"@esbuild/netbsd-arm64/bin/esbuild": "b4d529ea102b3d8e1d77f136be1d8b8496d641c216db8e4ce314f01b9ba02cd1",
"@esbuild/netbsd-x64/bin/esbuild": "ef999b6e59c2f7ed319f0bc804bffd9c5fec4dd7fd60c77f52300a7ffde2f12d",
"@esbuild/openbsd-arm64/bin/esbuild": "5de4790175cb306939af247b0a33036067a2677162d1a79635c77161c13cebee",
"@esbuild/openbsd-x64/bin/esbuild": "1b0dffef766fd0443f76b3031535a79ebf91050e0438399569539f9eae940d24",
"@esbuild/sunos-x64/bin/esbuild": "81ded843321d3d7a51986859ce859ea8d497e6fa4e3400c9d5ba3df0671a12df",
"@esbuild/win32-arm64/esbuild.exe": "ab1477cf488a7f961c3cd7da0689e4a78f8cac3c30eb1021ca0dba46aab25493",
"@esbuild/win32-ia32/esbuild.exe": "6b687d79c8d59bee797ac9872f3e25f229451059e12e60951f476a32d0e8ee71",
"@esbuild/win32-x64/esbuild.exe": "2ccfd6d5292493a810d21c0b40f9de3ff5252a0a24eb5dea3df07f33dcea0c0e"
}
}

469
package-lock.json generated Normal file
View File

@ -0,0 +1,469 @@
{
"name": "grechka-game",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"esbuild": "^0.28.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"esbuild": "^0.28.0"
}
}

View File

@ -1,5 +1,7 @@
// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ====================
// Возможность переопределить сервер через query string
// This module only sets SERVER_URL. Player name and world ID are handled in main.js
// to avoid duplicate prompts.
const urlParams = new URLSearchParams(window.location.search);
export const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru';
@ -8,60 +10,3 @@ if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) {
console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP');
alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.');
}
// ==================== WORLD ID И ИГРОКА ====================
let worldId = null;
let playerName = localStorage.getItem('minegrechka_playerName') || null;
// Запрашиваем имя игрока, если его нет
if (!playerName) {
playerName = prompt('Введите ваше имя для игры:') || 'Игрок';
localStorage.setItem('minegrechka_playerName', playerName);
console.log('Player name set:', playerName);
}
// Берём worldId из URL или генерируем новый
console.log('Current URL:', window.location.href);
const worldParam = urlParams.get('world');
console.log('world param:', worldParam);
// Проверяем на null, undefined или пустую строку
worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null;
console.log('worldId after params:', worldId, 'type:', typeof worldId);
// Если worldId отсутствует - генерируем новый и записываем в URL
if (!worldId) {
worldId = Math.random().toString(36).substring(2, 10);
console.log('Generated worldId:', worldId);
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', worldId);
const newUrlString = newUrl.toString();
console.log('New URL to set:', newUrlString);
// Проверяем, поддерживается ли history API
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', newUrlString);
console.log('URL after replaceState:', window.location.href);
console.log('URL after replaceState (direct check):', window.location.search);
} else {
console.error('History API not supported!');
}
} catch (e) {
console.error('Error updating URL:', e);
}
console.log('Generated new worldId for browser:', worldId);
}
console.log('Final worldId:', worldId, 'Player name:', playerName);
console.log(`Server URL: ${SERVER_URL}, World ID: ${worldId}`);
// Setters для изменения worldId/playerName из других модулей
export function setWorldId(id) { worldId = id; }
export function setPlayerName(name) { playerName = name; }
export { worldId, playerName };

View File

@ -1,27 +1,41 @@
// ==================== ХОЛСТ И РАЗМЕРЫ ====================
import { state } from './state.js';
const gameEl = document.getElementById('game');
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
export function initCanvas() {
const gameEl = document.getElementById('game');
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// offscreen light map (не вставляем в DOM)
const lightC = document.createElement('canvas');
const lightCtx = lightC.getContext('2d');
// offscreen light map
const lightC = document.createElement('canvas');
const lightCtx = lightC.getContext('2d');
const dpr = Math.max(1, window.devicePixelRatio || 1);
let W = 0, H = 0;
const dpr = Math.max(1, window.devicePixelRatio || 1);
function resize() {
W = gameEl.clientWidth;
H = gameEl.clientHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
lightC.width = W * dpr;
lightC.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
function resize() {
state.W = gameEl.clientWidth;
state.H = gameEl.clientHeight;
canvas.width = state.W * dpr;
canvas.height = state.H * dpr;
lightC.width = state.W * dpr;
lightC.height = state.H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// Store everything on state
state.gameEl = gameEl;
state.canvas = canvas;
state.ctx = ctx;
state.lightC = lightC;
state.lightCtx = lightCtx;
state.dpr = dpr;
}
function setW(v) { W = v; }
function setH(v) { H = v; }
export { canvas, ctx, lightC, lightCtx, dpr, W, H, gameEl, resize, setW, setH };
// Convenience exports for modules that use canvas directly
export function getCanvas() { return state.canvas; }
export function getCtx() { return state.ctx; }
export function getLightC() { return state.lightC; }
export function getLightCtx() { return state.lightCtx; }

View File

@ -1,36 +1,80 @@
import { TILE } from './constants.js';
import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X, GRAV, GRAV_WATER, MOVE, JUMP, DAY_LEN, SAVE_KEY, MOVE_SEND_INTERVAL, MAX_RAINDROPS, MAX_CHAT_MESSAGES, VOICE_SERVER } from './constants.js';
// Все мутабельные переменные игры в одном объекте состояния
export const state = {
// Константы (для доступа из модулей)
TILE,
SEA_GY,
BEDROCK_GY,
GEN_MARGIN_X,
GRAV,
GRAV_WATER,
MOVE,
JUMP,
DAY_LEN,
SAVE_KEY,
MOVE_SEND_INTERVAL,
MAX_RAINDROPS,
MAX_CHAT_MESSAGES,
VOICE_SERVER,
// Камера
camX: 0,
camY: 0,
W: 0,
H: 0,
dpr: 1,
// Canvas/DOM (заполняется в main.js)
gameEl: null,
canvas: null,
ctx: null,
lightC: null,
lightCtx: null,
// День/ночь
worldTime: 0,
isNightTime: false,
dayNightCycle: true,
// Мультиплеер
isMultiplayer: false,
mySocketId: null,
socket: null,
voiceSocket: null,
voicePosT: 0,
// Сервер
SERVER_URL: '',
TELEGRAM_BOT_USERNAME: '',
TELEGRAM_APP_SHORT_NAME: '',
// Игрок
playerName: null,
worldId: null,
// Инвентарь/UI
selected: 0,
selected: "dirt",
showFullInventory: false,
craftOpen: false,
inventoryOpen: false,
chatOpen: false,
minimapOpen: false,
modeIdx: 0,
currentFurnaceKey: null,
// Мир
worldSeed: Math.floor(Math.random() * 1000000),
// Погода
weatherType: "clear",
isRaining: false,
rainIntensity: 0,
weatherTimer: 0,
weatherChangeInterval: 60 + Math.random() * 120,
snowflakes: [],
lightningFlash: 0,
lightningTimer: 0,
// Мобы/спавн
spawnT: 0,
@ -44,6 +88,33 @@ export const state = {
lastSentX: 0,
lastSentY: 0,
// Mining progress
mining: { gx: null, gy: null, progress: 0, hardness: 0 },
// Trade/Market
myTgId: null,
tradePanel: null,
tradeContent: null,
marketPanel: null,
marketContent: null,
// DOM-ссылки (заполняются в main.js)
hpEl: null,
foodEl: null,
sxEl: null,
syEl: null,
todEl: null,
worldIdEl: null,
playerCountEl: null,
deathEl: null,
hotbarEl: null,
craftPanel: null,
recipesEl: null,
inventoryPanel: null,
inventoryGrid: null,
multiplayerStatus: null,
tempEl: null,
// Игрок
player: {
x: 6 * TILE,
@ -64,7 +135,9 @@ export const state = {
sleeping: false,
inBoat: false,
armor: 0,
equippedArmor: null
equippedArmor: null,
temperature: 15,
slowTimer: 0
},
// Точка спавна
@ -80,10 +153,21 @@ export const state = {
meat: 0, cooked: 0, arrow: 0,
wood_pickaxe: 0, stone_pickaxe: 0, iron_pickaxe: 0,
wood_sword: 0, stone_sword: 0, iron_sword: 0,
iron_armor: 0,
wood_axe: 0, stone_axe: 0, iron_axe: 0,
wood_shovel: 0, stone_shovel: 0, iron_shovel: 0,
iron_armor: 0, gold_armor: 0,
bow: 0, furnace: 0,
bed: 0, boat: 0,
iron_ingot: 0, gold_ingot: 0, copper_ingot: 0
iron_ingot: 0, gold_ingot: 0, copper_ingot: 0,
cobblestone: 0,
// New items
chicken_meat: 0, feather: 0, bone: 0, gunpowder: 0,
wheat: 0, bread: 0, carrot: 0, potato: 0, baked_potato: 0,
mushroom_stew: 0,
scorpion_stinger: 0, polar_fur: 0, slime_ball: 0, eagle_feather: 0,
snow: 0, ice: 0, cactus: 0, mushroom: 0, moss: 0, farmland: 0,
spruce_leaves: 0, dead_bush: 0, swamp_water: 0,
wheat_seeds: 0, wheat_stage0: 0, carrot_stage0: 0, potato_stage0: 0
},
// Лодка
@ -96,7 +180,7 @@ export const state = {
},
// Ввод
inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false },
inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false },
// Мышь
mouse: { x: 0, y: 0 },
@ -142,16 +226,25 @@ export const state = {
// Прочность инструментов
toolDurability: new Map(),
protections: new Map(),
// Последние выбранные предметы
recentItems: [],
// Активные печи
activeFurnaces: new Map(),
cropTimers: new Map(),
// Сгенерированные колонны
generated: new Set(),
// Изображение героя
heroImg: null
};
heroImg: new Image(),
// Сетка мира
grid: new Map(),
blocks: [],
// In-memory save fallback
inMemorySave: null,
};

View File

@ -1,30 +1,55 @@
export const BLOCKS = {
air: { n:'Воздух', solid:false },
grass: { n:'Трава', c:'#7cfc00', solid:true },
dirt: { n:'Грязь', c:'#8b4513', solid:true },
stone: { n:'Камень', c:'#7f8c8d', solid:true },
sand: { n:'Песок', c:'#f4d06f', solid:true },
gravel: { n:'Гравий', c:'#95a5a6', solid:true },
clay: { n:'Глина', c:'#74b9ff', solid:true },
wood: { n:'Дерево', c:'#d35400', solid:true },
planks: { n:'Доски', c:'#e67e22', solid:true },
ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true },
leaves: { n:'Листва', c:'#2ecc71', solid:true },
glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 },
water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true },
coal: { n:'Уголь', c:'#2c3e50', solid:true },
copper_ore:{ n:'Медь', c:'#e17055', solid:true },
iron_ore: { n:'Железо', c:'#dcdde1', solid:true },
iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5 },
gold_ore: { n:'Золото', c:'#f1c40f', solid:true },
diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true },
brick: { n:'Кирпич', c:'#c0392b', solid:true },
tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true },
campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:190 },
torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:140 },
bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true },
flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true },
bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true },
boat: { n:'Лодка', c:'#8B4513', solid:false },
furnace: { n:'Печь', c:'#696969', solid:true, smelting:true }
};
air: { n:'Воздух', solid:false, hardness:0 },
grass: { n:'Трава', c:'#7cfc00', solid:true, hardness:3, toolType:'shovel' },
dirt: { n:'Грязь', c:'#8b4513', solid:true, hardness:3, toolType:'shovel' },
stone: { n:'Камень', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' },
sand: { n:'Песок', c:'#f4d06f', solid:true, hardness:2, toolType:'shovel' },
gravel: { n:'Гравий', c:'#95a5a6', solid:true, hardness:3, toolType:'shovel' },
clay: { n:'Глина', c:'#74b9ff', solid:true, hardness:3, toolType:'shovel' },
wood: { n:'Дерево', c:'#d35400', solid:true, hardness:4, toolType:'axe' },
planks: { n:'Доски', c:'#e67e22', solid:true, hardness:3, toolType:'axe' },
ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true, hardness:1, toolType:null },
leaves: { n:'Листва', c:'#2ecc71', solid:true, hardness:1, toolType:'axe', minTool:null },
glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55, hardness:1, toolType:null },
water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true, hardness:0 },
coal: { n:'Уголь', c:'#2c3e50', solid:true, hardness:8, toolType:'pickaxe', minTool:'wood' },
copper_ore:{ n:'Медь', c:'#e17055', solid:true, hardness:10, toolType:'pickaxe', minTool:'wood' },
iron_ore: { n:'Железо', c:'#dcdde1', solid:true, hardness:12, toolType:'pickaxe', minTool:'stone' },
iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5, hardness:0 },
gold_ore: { n:'Золото', c:'#f1c40f', solid:true, hardness:15, toolType:'pickaxe', minTool:'stone' },
diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true, hardness:20, toolType:'pickaxe', minTool:'iron' },
brick: { n:'Кирпич', c:'#c0392b', solid:true, hardness:8, toolType:'pickaxe' },
tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true, hardness:1, toolType:null },
campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280, hardness:2, toolType:null },
torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220, hardness:1, toolType:null },
bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true, hardness:Infinity },
flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true, hardness:0 },
bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true, hardness:2, toolType:'axe' },
boat: { n:'Лодка', c:'#8B4513', solid:false, hardness:0 },
furnace: { n:'Печь', c:'#696969', solid:true, smelting:true, hardness:8, toolType:'pickaxe', minTool:'wood' },
cobblestone: { n:'Булыжник', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' },
flag: { n:'Флаг', c:'#e74c3c', solid:true, hardness:1, toolType:'axe', protectionRadius:10 },
// === BIOME BLOCKS ===
snow: { n:'Снег', c:'#ecf0f1', solid:true, hardness:1, toolType:'shovel' },
ice: { n:'Лёд', c:'#74b9ff', solid:true, hardness:1, slip:true, toolType:'pickaxe' },
cactus: { n:'Кактус', c:'#27ae60', solid:true, hardness:1, hurt:true, toolType:null },
mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true, hardness:0 },
moss: { n:'Мох', c:'#0a6640', solid:true, hardness:1, toolType:null },
swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true, hardness:0 },
farmland: { n:'Грядка', c:'#8B6914', solid:true, hardness:1, farmable:true, toolType:'shovel' },
dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true, hardness:0 },
spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true, hardness:1, toolType:'axe' },
// === CROP STAGES ===
wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'wheat_stage1' },
wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'wheat_stage2' },
wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'wheat_stage3' },
wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'wheat', harvestQty:2 },
carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'carrot_stage1' },
carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'carrot_stage2' },
carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'carrot_stage3' },
carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'carrot', harvestQty:3 },
potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'potato_stage1' },
potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'potato_stage2' },
potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'potato_stage3' },
potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'potato', harvestQty:2 },
};

View File

@ -2,9 +2,36 @@ export const ITEMS = {
meat: { n:'Сырое мясо', icon:'🥩', food:15 },
cooked: { n:'Жареное мясо', icon:'🍖', food:45 },
arrow: { n:'Стрела', icon:'➡️', stack:64 },
chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 },
feather: { n:'Перо', icon:'🪶', stack:64 },
bone: { n:'Кость', icon:'🦴', stack:64 },
gunpowder: { n:'Порох', icon:'💥', stack:64 },
// === FARMING ITEMS ===
wheat: { n:'Пшеница', icon:'🌾', stack:64 },
bread: { n:'Хлеб', icon:'🍞', food:30 },
carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 },
potato: { n:'Картофель', icon:'🥔', stack:64 },
baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 },
wheat_seeds: { n:'Семена пшеницы', icon:'🌱', stack:64 },
mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 },
// === MOB DROP ITEMS ===
scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 },
polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 },
slime_ball: { n:'Слизь', icon:'🟢', stack:64 },
eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 },
// === ARMOR ===
gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 },
};
// Runtime extensions — предметы от обжига
ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' };
ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' };
ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' };
ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' };
// Axes and shovels
ITEMS.wood_axe = { n:'Деревянный топор', icon:'🪓' };
ITEMS.stone_axe = { n:'Каменный топор', icon:'🪓' };
ITEMS.iron_axe = { n:'Железный топор', icon:'🪓' };
ITEMS.wood_shovel = { n:'Деревянная лопата', icon:'🔱' };
ITEMS.stone_shovel = { n:'Каменная лопата', icon:'🔱' };
ITEMS.iron_shovel = { n:'Железная лопата', icon:'🔱' };

View File

@ -14,19 +14,31 @@ export const RECIPES = [
{ out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } },
{ out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } },
{ out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } },
{ out:'wood_axe', qty:1, cost:{ wood:3, planks:2 } },
{ out:'stone_axe', qty:1, cost:{ wood:1, stone:3 } },
{ out:'iron_axe', qty:1, cost:{ wood:1, iron_ore:2 } },
{ out:'wood_shovel', qty:1, cost:{ wood:1, planks:1 } },
{ out:'stone_shovel', qty:1, cost:{ wood:1, stone:2 } },
{ out:'iron_shovel', qty:1, cost:{ wood:1, iron_ore:1 } },
{ out:'iron_armor', qty:1, cost:{ iron_ore: 5 } },
{ out:'gold_armor', qty:1, cost:{ gold_ore: 8 } },
{ out:'furnace', qty:1, cost:{ stone: 8 } },
{ out:'bow', qty:1, cost:{ wood: 3, planks: 2 } },
{ out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } }
{ out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } },
{ out:'flag', qty:1, cost:{ planks:3, iron_ore:1 } },
{ out:'bread', qty:1, cost:{ wheat: 3 } },
{ out:'farmland', qty:1, cost:{ dirt:1 } },
{ out:'wheat_seeds', qty:1, cost:{ farmland:1, dirt:1 } },
];
// Рецепты печи (обжиг)
export const SMELTING_RECIPES = [
{ in:'sand', qty:1, out:'glass', outQty:1, time:3 }, // песок → стекло
{ in:'clay', qty:1, out:'brick', outQty:1, time:3 }, // глина → кирпич
{ in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, // железо → слиток
{ in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, // золото → слиток
{ in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, // медь → слиток
{ in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, // сырое мясо → жареное
{ in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 } // булыжник → камень
];
{ in:'sand', qty:1, out:'glass', outQty:1, time:3 },
{ in:'clay', qty:1, out:'brick', outQty:1, time:3 },
{ in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 },
{ in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 },
{ in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 },
{ in:'meat', qty:1, out:'cooked', outQty:1, time:2 },
{ in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 },
{ in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 }
];

View File

@ -1,14 +1,21 @@
export const TOOLS = {
wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } },
stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } },
iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } },
wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 } },
stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 } },
iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } },
bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, string: 0 } }
wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', damage:1, durability:60, toolType:'pickaxe', toolTier:'wood', speed:2 },
stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', damage:2, durability:132, toolType:'pickaxe', toolTier:'stone', speed:4 },
iron_pickaxe: { n:'Железная кирка', icon:'⛏️', damage:3, durability:250, toolType:'pickaxe', toolTier:'iron', speed:6 },
wood_sword: { n:'Деревянный меч', icon:'🗡️', damage:4, durability:60 },
stone_sword: { n:'Каменный меч', icon:'🗡️', damage:5, durability:132 },
iron_sword: { n:'Железный меч', icon:'🗡️', damage:7, durability:250 },
wood_axe: { n:'Деревянный топор', icon:'🪓', damage:3, durability:60, toolType:'axe', toolTier:'wood', speed:2 },
stone_axe: { n:'Каменный топор', icon:'🪓', damage:4, durability:132, toolType:'axe', toolTier:'stone', speed:4 },
iron_axe: { n:'Железный топор', icon:'🪓', damage:5, durability:250, toolType:'axe', toolTier:'iron', speed:6 },
wood_shovel: { n:'Деревянная лопата', icon:'🔱', damage:1, durability:60, toolType:'shovel', toolTier:'wood', speed:2 },
stone_shovel: { n:'Каменная лопата', icon:'🔱', damage:2, durability:132, toolType:'shovel', toolTier:'stone', speed:4 },
iron_shovel: { n:'Железная лопата', icon:'🔱', damage:3, durability:250, toolType:'shovel', toolTier:'iron', speed:6 },
bow: { n:'Лук', icon:'🏹', damage:10, durability:200 },
};
import { state } from '../core/state.js';
import { BLOCKS } from './blocks.js';
export function addTool(type) {
const maxDur = TOOLS[type]?.durability || 60;
@ -29,4 +36,31 @@ export function useTool(type) {
}
}
return false; // no durability entry found
}
export function getBestTool(blockType) {
const block = BLOCKS[blockType];
if (!block || !block.toolType) return null;
const toolType = block.toolType;
for (const tier of ['iron', 'stone', 'wood']) {
const toolId = `${tier}_${toolType}`;
if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId;
}
return null;
}
export function toolTierLessThan(toolTier, minTier) {
const tiers = { wood: 1, stone: 2, iron: 3 };
return (tiers[toolTier] || 0) < (tiers[minTier] || 0);
}
export function getMiningSpeed(blockType) {
const block = BLOCKS[blockType];
if (!block) return 1;
const toolId = getBestTool(blockType);
if (toolId) {
const tool = TOOLS[toolId];
if (tool.toolType === block.toolType) return tool.speed;
}
return 1; // Bare hands = 1 click per hit
}

View File

@ -7,18 +7,18 @@ import { playSound } from '../audio/sound-engine.js';
import { explodeAt } from '../world/tnt.js';
import { resolveY } from '../physics/collision.js';
import { resolveX } from '../physics/collision.js';
import { isSolid } from '../world/world-storage.js';
import { getCachedBiome } from '../world/generation.js';
export function mobAI(m, dt) {
updateWaterFlag(m);
if (m.kind === 'zombie') {
// активность ночью
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
// атака
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
state.player.invuln <= 0) {
@ -27,20 +27,15 @@ export function mobAI(m, dt) {
state.player.invuln = 0.8;
state.player.vx += dir * 420;
state.player.vy -= 260;
playSound('hit1'); // Звук при атаке зомби
playSound('hit1');
}
} else if (m.kind === 'creeper') {
// активность ночью
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
// Движение к игроку
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
// Взрыв если близко к игроку
if (dist < 60) {
m.fuse -= dt;
if (m.fuse <= 0) {
@ -48,23 +43,17 @@ export function mobAI(m, dt) {
m.hp = 0;
}
} else {
// Поджигаем если очень близко
if (dist < 40) {
m.fuse = 0.5; // Быстрый взрыв
m.fuse = 0.5;
}
}
} else if (m.kind === 'skeleton') {
// активность ночью
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
// Движение к игроку
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
// Стрельба стрелами
m.shootCooldown -= dt;
if (dist < 300 && m.shootCooldown <= 0) {
m.shootCooldown = 2.0;
@ -82,8 +71,72 @@ export function mobAI(m, dt) {
life: 3
});
}
} else if (m.kind === 'scorpion') {
// Скорпион — бежит к игроку, яд (замедление)
const dir = Math.sign((state.player.x) - m.x);
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
state.player.invuln <= 0) {
const damage = calculateDamage(8);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.slowTimer = 3;
state.player.vx += dir * 300;
state.player.vy -= 200;
playSound('hit1');
}
} else if (m.kind === 'polar_bear') {
// Белый медведь — нейтрален, бродит
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 2.0 + Math.random() * 3;
m.dir = Math.random() < 0.5 ? -1 : 1;
if (Math.random() < 0.3) m.dir = 0;
}
m.vx = m.dir * m.speed;
if (m.inWater) m.vy = -120;
} else if (m.kind === 'slime') {
// Слизь — прыгает к игроку
const dir = Math.sign((state.player.x + state.player.w / 2) - (m.x + m.w / 2));
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 1.5 + Math.random() * 1.5;
m.dir = dir;
m.vy = -200;
}
m.vx = m.dir * m.speed;
} else if (m.kind === 'eagle') {
// Орёл — летает, атакует пикированием
const dx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2);
const dy = (state.player.y + state.player.h / 2) - (m.y + m.h / 2);
const dist = Math.hypot(dx, dy);
if (dist < 400) {
m.vx = Math.sign(dx) * m.speed;
m.vy = dy > 0 ? 60 : -60;
} else {
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 2 + Math.random() * 3;
m.dir = Math.random() < 0.5 ? -1 : 1;
}
m.vx = m.dir * m.speed * 0.5;
m.vy = Math.sin(performance.now() / 1000) * 30;
}
// Атака при касании
if (dist < 30 && state.player.invuln <= 0) {
const damage = calculateDamage(10);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.vy -= 200;
playSound('hit1');
}
// Орёл не падает — летающий моб
m.vy *= 0.5;
m.grounded = false;
} else {
// животные
// животные (pig, chicken)
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 1.8 + Math.random() * 2.5;
@ -94,8 +147,17 @@ export function mobAI(m, dt) {
if (m.inWater) m.vy = -120;
}
// Мобы прыгают при столкновении со стеной
if (m.grounded && Math.abs(m.vx) > 5 && m.kind !== 'eagle') {
const probeX = m.vx > 0 ? m.x + m.w + 2 : m.x - 2;
const probeGY = Math.floor((m.y + m.h - 1) / TILE);
if (isSolid(Math.floor(probeX / TILE), probeGY)) {
m.vy = -280;
}
}
// физика моба
const g = m.inWater ? GRAV_WATER : GRAV;
const g = (m.inWater && m.kind !== 'eagle') ? GRAV_WATER : GRAV;
m.vy += g * dt;
m.y += m.vy * dt; m.grounded = false; resolveY(m);
@ -103,6 +165,21 @@ export function mobAI(m, dt) {
}
export function isNight() {
// Автоматический цикл: ночь когда worldTime > 0.5
return state.worldTime > 0.5;
}
}
// Mob loot table
export function getMobLoot(kind) {
const table = {
chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}],
pig: [{item:'meat',min:1,max:2,chance:1}],
zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}],
skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}],
creeper: [{item:'gunpowder',min:1,max:2,chance:1}],
scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}],
polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}],
slime: [{item:'slime_ball',min:1,max:2,chance:1}],
eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}]
};
return table[kind] || [];
}

View File

@ -1,4 +1,4 @@
// Сущности: животные + зомби
// Сущности: животные + зомби + новые мобы
export class Entity {
constructor(x, y, w, h) {
@ -34,4 +34,20 @@ export class Creeper extends Entity {
export class Skeleton extends Entity {
constructor(x, y) { super(x, y, 34, 50); this.kind = 'skeleton'; this.hp = 4; this.speed = 70 + Math.random() * 30; this.shootCooldown = 0; }
}
}
export class Scorpion extends Entity {
constructor(x, y) { super(x, y, 26, 26); this.kind = 'scorpion'; this.hp = 3; this.speed = 90; this.fuse = 0; this.shootCooldown = 0; }
}
export class PolarBear extends Entity {
constructor(x, y) { super(x, y, 40, 34); this.kind = 'polar_bear'; this.hp = 8; this.speed = 50; this.fuse = 0; this.shootCooldown = 2; }
}
export class Slime extends Entity {
constructor(x, y) { super(x, y, 24, 24); this.kind = 'slime'; this.hp = 2; this.speed = 30; this.fuse = 0; this.shootCooldown = 2; }
}
export class Eagle extends Entity {
constructor(x, y) { super(x, y, 30, 22); this.kind = 'eagle'; this.hp = 3; this.speed = 120; this.fuse = 0; this.shootCooldown = 0; }
}

View File

@ -3,12 +3,8 @@ import { state } from '../core/state.js';
// Функция для расчёта урона с учётом брони
function calculateDamage(baseDamage) {
// Броня снижает урон пропорционально
// armor: 0 = без брони (100% урона)
// armor: 0.5 = железная броня (50% урона)
const reduction = state.player.armor;
const actualDamage = baseDamage * (1 - reduction);
console.log('[DAMAGE] Base:', baseDamage, '- Armor:', reduction, '- Actual:', actualDamage.toFixed(1));
return actualDamage;
}
@ -32,12 +28,13 @@ function initPlayer() {
lastStepTime: 0,
sleeping: false,
inBoat: false,
armor: 0, // Текущий уровень защиты (0 = без брони, 0.5 = железная броня)
equippedArmor: null // Тип надетой брони
armor: 0,
equippedArmor: null,
temperature: 15,
slowTimer: 0
};
// Сохраняем начальную позицию для возрождения
state.spawnPoint = { x: 6 * TILE, y: 0 * TILE };
}
export { calculateDamage, initPlayer };
export { calculateDamage, initPlayer };

View File

@ -3,16 +3,16 @@ import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { playSound } from '../audio/sound-engine.js';
import { getBlock } from '../world/world-storage.js';
import { updateWaterPhysics } from '../physics/water.js';
import { updateWaterPhysics } from '../world/water.js';
import { updateWaterFlag } from '../physics/water-detect.js';
import { resolveY, resolveX } from '../physics/collision.js';
import { calculateDamage } from '../entities/player.js';
import { isNight } from '../entities/mob-ai.js';
import { tickFurnaces } from '../ui/furnace.js';
import { renderFurnaceUI } from '../ui/furnace.js';
import { updateWeather, updateRain, drawRain } from '../world/weather.js';
import { ensureGenAroundCamera, surfaceGyAt, genColumn } from '../world/generation.js';
import { Zombie, Creeper, Skeleton, Pig, Chicken } from '../entities/mobs.js';
import { updateWeather, updateRain, drawWeather, skyColor } from '../world/weather.js';
import { ensureGenAroundCamera, surfaceGyAt, genColumn, getCachedBiome, getBiome } from '../world/generation.js';
import { Zombie, Creeper, Skeleton, Pig, Chicken, Scorpion, PolarBear, Slime, Eagle } from '../entities/mobs.js';
import { mobAI } from '../entities/mob-ai.js';
import { explodeAt } from '../world/tnt.js';
import { sendPlayerPosition } from '../multiplayer/socket-helpers.js';
@ -21,6 +21,7 @@ import { rebuildHotbar } from '../ui/hotbar.js';
import { mode } from '../game/modes.js';
import { mouse } from '../input/mouse-handler.js';
import { drawFire } from '../render/draw-fire.js';
import { itemTex } from '../render/textures.js';
// ==================== LOOP ====================
let last = performance.now();
@ -52,12 +53,12 @@ export function loop(now) {
const jumpPressed = inp.j && !prevJump;
prevJump = inp.j;
// Ускорение времени во время сна
if (player.sleeping && isNight()) {
state.worldTime += dt * 8 / state.DAY_LEN; // В 8 раз быстрее
// Восстанавливаем здоровье во время сна
// Ускорение времени во время сна или замороженный цикл
if (!state.dayNightCycle) {
// Цикл день/ночь выключен — время стоит
} else if (player.sleeping && isNight()) {
state.worldTime += dt * 8 / state.DAY_LEN;
player.hp = Math.min(100, player.hp + dt * 20);
// Автоматическое пробуждение когда наступает день
if (!isNight()) {
player.sleeping = false;
}
@ -70,6 +71,19 @@ export function loop(now) {
state.camX = Math.floor((player.x + player.w / 2) - state.W / 2);
state.camY = Math.floor((player.y + player.h / 2) - state.H / 2);
// Самовосстановление при битых координатах (NaN/undefined из старого сейва):
// иначе камера уезжает в NaN — видно только небо, без блоков и персонажа.
if (!Number.isFinite(player.x) || !Number.isFinite(player.y)) {
const startGX = 6;
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
player.x = startGX * state.TILE;
player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
player.vx = player.vy = 0;
state.camX = Math.floor((player.x + player.w / 2) - state.W / 2);
state.camY = Math.floor((player.y + player.h / 2) - state.H / 2);
console.warn('[Loop] player pos was broken, respawned at start');
}
ensureGenAroundCamera();
// clouds parallax
@ -81,19 +95,96 @@ export function loop(now) {
// player
updateWaterFlag(player);
// кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем
// кислород/утопление
if (player.headInWater) {
player.o2 = Math.max(0, player.o2 - 6 * dt); // замедлил в 3.7 раза
player.o2 = Math.max(0, player.o2 - 6 * dt);
if (player.o2 === 0) {
const damage = calculateDamage(4 * dt);
player.hp -= damage;
}
} else {
player.o2 = Math.min(100, player.o2 + 10 * dt); // замедлил восстановление в 4 раза
player.o2 = Math.min(100, player.o2 + 10 * dt);
}
// голод убывает, но HP не отнимает (как просили)
player.hunger = Math.max(0, player.hunger - dt * 0.2); // замедлил в 4 раза
// голод убывает
player.hunger = Math.max(0, player.hunger - dt * 0.2);
// ========== TEMPERATURE ==========
if (player.temperature === undefined) player.temperature = 15;
const BIOME_TEMP = { snow: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 };
const COLD_THRESHOLD = -5;
const HEAT_THRESHOLD = 35;
const HEAT_RADIUS = 5;
const pGX = Math.floor(player.x / state.TILE);
const biome = getCachedBiome(pGX);
const pGY = Math.floor(player.y / state.TILE);
const sGY = surfaceGyAt(pGX);
const isUndergroundTemp = (pGY - sGY) > 2;
let targetTemp = BIOME_TEMP[biome] || 15;
if (isUndergroundTemp) targetTemp = 15;
if (isNight() && !isUndergroundTemp) targetTemp -= 10;
if ((state.weatherType === 'rain' || state.weatherType === 'storm') && !isUndergroundTemp) targetTemp -= 5;
player.temperature += (targetTemp - player.temperature) * dt * 0.3;
let nearHeat = false, nearCool = false;
for (let dx = -HEAT_RADIUS; dx <= HEAT_RADIUS; dx++) {
for (let dy = -HEAT_RADIUS; dy <= HEAT_RADIUS; dy++) {
const b = getBlock(pGX + dx, pGY + dy);
if (!b || b.dead) continue;
if (b.t === 'campfire' || b.t === 'torch') nearHeat = true;
if (b.t === 'water' && biome === 'desert') nearCool = true;
}
}
if (nearHeat) {
player.temperature += (30 - player.temperature) * dt * 2;
if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt);
}
if (nearCool) {
player.temperature += (15 - player.temperature) * dt * 1.5;
if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) player.hp = Math.min(100, player.hp + 8 * dt);
}
if (isUndergroundTemp && !nearHeat) {
if (player.hp < 100 && player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) {
player.hp = Math.min(100, player.hp + 2 * dt);
}
}
const aboveBlk = getBlock(pGX, pGY - 1);
const inShade = aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass';
if (inShade && !isUndergroundTemp && !nearCool) {
player.temperature -= 5 * dt;
if (player.temperature > HEAT_THRESHOLD - 2 && player.hp < 100) player.hp = Math.min(100, player.hp + 1 * dt);
}
if (player.temperature < COLD_THRESHOLD && !isUndergroundTemp) {
const severity = Math.abs(player.temperature - COLD_THRESHOLD) / 15;
player.hp -= 1 * severity * dt;
if (severity > 0.5) player.vx *= (1 - 0.3 * Math.min(1, severity) * dt);
}
if (player.temperature > HEAT_THRESHOLD && !isUndergroundTemp) {
const severity = (player.temperature - HEAT_THRESHOLD) / 15;
player.hp -= 1 * severity * dt;
if (severity > 0.5) player.hunger -= 0.5 * severity * dt;
}
if (player.slowTimer > 0) player.slowTimer -= dt;
// Кактус — урон при касании
const pCGX = Math.floor((player.x + player.w / 2) / state.TILE);
const pCGY = Math.floor((player.y + player.h / 2) / state.TILE);
for (let cdx = -1; cdx <= 1; cdx++) {
for (let cdy = -1; cdy <= 1; cdy++) {
const cb = getBlock(pCGX + cdx, pCGY + cdy);
if (cb && !cb.dead && BLOCKS[cb.t] && BLOCKS[cb.t].hurt) {
if (player.invuln <= 0) {
player.hp -= 2;
player.invuln = 0.5;
}
}
}
}
// Лёд — скольжение
const iceBlock = getBlock(pCGX, pCGY + 1);
if (iceBlock && BLOCKS[iceBlock.t] && BLOCKS[iceBlock.t].slip && player.grounded) {
player.vx *= 0.98;
}
// Игрок не может двигаться во время сна
if (player.sleeping) {
@ -107,64 +198,50 @@ export function loop(now) {
// Звук шагов при движении по земле
if (player.grounded && !player.inWater && Math.abs(player.vx) > 50) {
const stepInterval = 0.35; // Интервал между шагами в секундах
const stepInterval = 0.35;
if (now / 1000 - player.lastStepTime > stepInterval) {
playSound('step');
player.lastStepTime = now / 1000;
}
}
// прыжок/плавание (новая логика)
// прыжок/плавание
if (player.inBoat) {
// Игрок в лодке - лодка следует за игроком
const dir = (inp.r ? 1 : 0) - (inp.l ? 1 : 0);
if (dir) boat.vx = dir * state.MOVE;
else boat.vx *= 0.95;
// Лодка плавает на воде
boat.vy = 0;
// Игрок следует за лодкой (сидит внутри неё)
player.x = boat.x + 2; // Игрок по центру лодки
player.y = boat.y - 4; // Игрок выше лодки (сидит внутри)
player.x = boat.x + 2;
player.y = boat.y - 4;
player.vx = boat.vx;
player.vy = boat.vy;
player.grounded = true;
player.inWater = false; // Игрок не в воде когда в лодке
player.inWater = false;
// Прыжок из лодки (высадка)
if (jumpPressed) {
// Возвращаем лодку в инвентарь
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
player.y += state.TILE; // Прыгаем из лодки
player.y += state.TILE;
player.vy = -state.JUMP * 0.5;
playSound('splash');
}
} else if (player.inWater) {
// сопротивление в воде
player.vx *= 0.90;
player.vy *= 0.92;
// Если не нажимаем прыжок - тонем (гравитация в воде)
if (!jumpPressed && !inp.j) {
// Применяем гравитацию в воде - игрок тонет
player.vy += state.GRAV_WATER * dt;
} else {
// Если нажимаем прыжок - поднимаемся на поверхность
if (jumpPressed) {
player.vy = Math.min(player.vy, -520); // рывок вверх
player.vy = Math.min(player.vy, -520);
} else if (inp.j) {
// если держим — мягкое всплытие
player.vy = Math.min(player.vy, -260);
}
}
} else {
// обычный прыжок (только по нажатию)
if (jumpPressed && player.grounded && !player.sleeping) {
player.vy = -state.JUMP;
player.grounded = false;
@ -172,7 +249,7 @@ export function loop(now) {
}
}
// Гравитация применяется только вне воды и вне лодки
// Гравитация
if (!player.inWater && !player.inBoat) {
player.vy += state.GRAV * dt;
}
@ -182,13 +259,11 @@ export function loop(now) {
boat.x += boat.vx * dt;
boat.y += boat.vy * dt;
// Лодка не выходит за пределы воды
const boatGX = Math.floor(boat.x / state.TILE);
const boatGY = Math.floor(boat.y / state.TILE);
const below = getBlock(boatGX, boatGY + 1);
if (!below || below.t !== 'water') {
// Если лодка вышла из воды - выкидываем игрока
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
@ -198,7 +273,6 @@ export function loop(now) {
}
}
// Проверяем, не доплыл ли игрок из лодки
if (player.inBoat && !boat.active) {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
@ -207,7 +281,7 @@ export function loop(now) {
playSound('splash');
}
// Sub-stepped physics: применяем движение мелкими шагами
// Sub-stepped physics
for (let step = 0; step < steps; step++) {
player.y += player.vy * dt;
resolveY(player);
@ -215,13 +289,10 @@ export function loop(now) {
resolveX(player);
}
// Отправляем позицию на сервер (мультиплеер)
sendPlayerPosition();
// Обновляем физику воды
updateWaterPhysics(dt);
// Погода и дождь
updateWeather(dt);
updateRain(dt);
@ -237,33 +308,28 @@ export function loop(now) {
// Furnace tick
tickFurnaces(dt);
// Обновляем UI печи если открыта
if (state.currentFurnaceKey && Math.random() < 0.1) {
renderFurnaceUI();
}
// Projectile tick (стрелы)
// Projectile tick
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * dt; // гравитация
p.vy += 400 * dt;
p.life -= dt;
// Столкновение с блоком
const gx = Math.floor(p.x / state.TILE);
const gy = Math.floor(p.y / state.TILE);
const blk = getBlock(gx, gy);
if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid) {
// Врезался в стену
if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++; // подбираем ~50%
if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++;
projectiles.splice(i, 1);
continue;
}
// Столкновение с сущностью
if (p.owner === 'mob') {
// Попал в игрока
if (p.x > player.x && p.x < player.x + player.w && p.y > player.y && p.y < player.y + player.h) {
if (player.invuln <= 0) {
player.hp -= calculateDamage(p.dmg);
@ -276,7 +342,6 @@ export function loop(now) {
continue;
}
} else {
// Попал в моба — server mobs first in multiplayer
let hitMob = false;
if (state.isMultiplayer) {
for (const [id, sm] of state.serverMobs) {
@ -290,7 +355,6 @@ export function loop(now) {
}
}
if (!hitMob) {
// Local mobs
for (let j = mobs.length - 1; j >= 0; j--) {
const m = mobs[j];
if (p.x > m.x && p.x < m.x + m.w && p.y > m.y && p.y < m.y + m.h) {
@ -313,7 +377,6 @@ export function loop(now) {
}
}
// Таймаут
if (p.life <= 0) projectiles.splice(i, 1);
}
@ -327,12 +390,35 @@ export function loop(now) {
}
}
// mobs spawn (с обеих сторон камеры) — только в одиночном режиме
// ====== CROP GROWTH ======
for (const [key, blk] of state.grid) {
if (blk.dead) continue;
const bDef = BLOCKS[blk.t];
if (bDef.growthStage === undefined) continue;
if (bDef.harvestable) continue; // stage 3 = fully grown
if (!blk.growthTimer && blk.growthTimer !== 0) blk.growthTimer = 0;
blk.growthTimer += dt;
// Growth time from block def (default 30s per stage)
const growTime = bDef.growthTime || 30;
if (blk.growthTimer >= growTime) {
blk.growthTimer = 0;
const nextStage = bDef.growsTo;
if (nextStage) {
blk.t = nextStage;
// Update texture reference
if (tex[nextStage]) {
// Block type changed — will render new texture next frame
}
}
}
}
// mobs spawn — only in singleplayer
state.spawnT += dt;
if (!state.isMultiplayer && state.spawnT > 1.8 && mobs.length < 30) {
state.spawnT = 0;
// Выбираем сторону спавна (левая или правая)
const spawnLeft = Math.random() < 0.5;
const gx = spawnLeft
? Math.floor((state.camX - 200) / state.TILE)
@ -343,18 +429,21 @@ export function loop(now) {
const wx = gx * state.TILE + 4;
const wy = (sgy - 2) * state.TILE;
// не спавнить в воде
const top = getBlock(gx, sgy);
if (top && top.t === 'water') {
// skip
} else {
const night = isNight();
const spawnBiome = getCachedBiome(gx);
if (night) {
// Ночью спавним враждебных мобов (максимум 12 хостайл)
const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton').length;
const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton' || m.kind === 'scorpion' || m.kind === 'slime').length;
if (hostileCount < 12) {
const rand = Math.random();
if (rand < 0.35) {
if (spawnBiome === 'desert' && rand < 0.6) {
mobs.push(new Scorpion(wx, wy));
} else if (spawnBiome === 'swamp' && rand < 0.5) {
mobs.push(new Slime(wx, wy));
} else if (rand < 0.35) {
mobs.push(new Zombie(wx, wy));
} else if (rand < 0.55) {
mobs.push(new Creeper(wx, wy));
@ -363,15 +452,21 @@ export function loop(now) {
}
}
}
// Животные спавнятся и днём и ночью (с лимитом)
const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken').length;
// Daytime biome mobs
const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken' || m.kind === 'polar_bear' || m.kind === 'eagle').length;
if (animalCount < 8) {
mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy));
if (spawnBiome === 'tundra' && Math.random() < 0.6) {
mobs.push(new PolarBear(wx, wy));
} else if (spawnBiome === 'mountains' && Math.random() < 0.4) {
mobs.push(new Eagle(wx, wy));
} else {
mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy));
}
}
}
}
// mobs update — только локальные (singleplayer)
// mobs update — только локальные
if (!state.isMultiplayer) {
for (let i = mobs.length - 1; i >= 0; i--) {
const m = mobs[i];
@ -394,7 +489,6 @@ export function loop(now) {
if (player.hp <= 0) {
state.deathEl.style.display = 'flex';
} else if (state.deathEl.style.display === 'flex') {
// Если HP > 0 но экран смерти всё ещё показан - скрываем его
state.deathEl.style.display = 'none';
}
@ -409,10 +503,10 @@ export function loop(now) {
const night = isNight();
// sky
ctx.fillStyle = night ? '#070816' : (state.isRaining ? '#6B7B8D' : '#87CEEB');
const playerGx = Math.floor(player.x / state.TILE); const skyBiome = getBiome(playerGx); ctx.fillStyle = skyColor(night, skyBiome, state.weatherType, state.worldTime);
ctx.fillRect(0, 0, W, H);
// clouds (parallax x/y)
// clouds
ctx.save();
ctx.translate(-camX * 0.5, -camY * 0.15);
ctx.fillStyle = 'rgba(255,255,255,0.65)';
@ -432,7 +526,7 @@ export function loop(now) {
const maxGY = Math.floor((camY + H) / TILE) + 6;
const blocks = state.blocks;
// draw blocks (по массиву, но фильтруем диапазоном)
// draw blocks
for (const b of blocks) {
if (b.dead) continue;
if (b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue;
@ -447,7 +541,7 @@ export function loop(now) {
ctx.drawImage(tex[b.t], b.gx * TILE, b.gy * TILE, TILE, TILE);
}
// TNT мигает, если активирован
// TNT мигает
if (b.t === 'tnt' && b.active && Math.sin(now / 60) > 0) {
ctx.fillStyle = 'rgba(255,255,255,0.45)';
ctx.fillRect(b.gx * TILE, b.gy * TILE, TILE, TILE);
@ -463,6 +557,42 @@ export function loop(now) {
}
}
// ====== MINING CRACK OVERLAY ======
if (state.mining.gx !== null && state.mining.hardness > 0) {
const mg = state.mining;
const progress = Math.min(1, mg.progress / mg.hardness);
const cx = mg.gx * TILE - camX + camX; // already in world coords since we translated
const cy = mg.gy * TILE - camY + camY;
// Actually, we're in translated context, so just use world coords
const wx = mg.gx * TILE;
const wy = mg.gy * TILE;
ctx.save();
ctx.globalAlpha = Math.min(0.8, progress);
// Draw crack lines
const numCracks = Math.floor(progress * 5);
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
for (let i = 0; i < numCracks; i++) {
ctx.beginPath();
ctx.moveTo(wx + TILE/2, wy + TILE/2);
const angle = (i * 72 + 36) * Math.PI / 180;
ctx.lineTo(wx + TILE/2 + Math.cos(angle) * TILE * 0.4, wy + TILE/2 + Math.sin(angle) * TILE * 0.4);
ctx.stroke();
}
// Darkening overlay
ctx.fillStyle = `rgba(0,0,0,${progress * 0.4})`;
ctx.fillRect(wx, wy, TILE, TILE);
// Progress bar under block
ctx.globalAlpha = 1;
ctx.fillStyle = '#333';
ctx.fillRect(wx, wy + TILE - 4, TILE, 4);
ctx.fillStyle = '#2ecc71';
ctx.fillRect(wx, wy + TILE - 4, TILE * progress, 4);
ctx.restore();
}
// mobs
const allMobsRender = state.isMultiplayer ? Array.from(state.serverMobs.values()) : mobs;
for (const m of allMobsRender) {
@ -480,7 +610,6 @@ export function loop(now) {
ctx.fillStyle = '#ff69b4';
ctx.fillRect(m.x + 28, m.y + 12, 6, 6);
} else if (m.kind === 'chicken') {
// chicken
ctx.fillStyle = '#ecf0f1';
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = '#f39c12';
@ -488,22 +617,16 @@ export function loop(now) {
ctx.fillStyle = '#000';
ctx.fillRect(m.x + 8, m.y + 6, 3, 3);
} else if (m.kind === 'creeper') {
// creeper
ctx.fillStyle = '#4CAF50';
ctx.fillRect(m.x, m.y, m.w, m.h);
// Глаза
ctx.fillStyle = '#000';
ctx.fillRect(m.x + 8, m.y + 8, 4, 4);
ctx.fillRect(m.x + 22, m.y + 8, 4, 4);
// Рот
ctx.fillStyle = '#000';
ctx.fillRect(m.x + 12, m.y + 20, 10, 4);
// Ноги
ctx.fillStyle = '#4CAF50';
ctx.fillRect(m.x + 4, m.y + 30, 6, 20);
ctx.fillRect(m.x + 24, m.y + 30, 6, 20);
} else if (m.kind === 'skeleton') {
// skeleton - детализированный
ctx.fillStyle = '#ECEFF1';
ctx.fillRect(m.x + 10, m.y + 20, 14, 12);
ctx.fillRect(m.x + 8, m.y + 0, 18, 18);
@ -516,7 +639,6 @@ export function loop(now) {
ctx.fillRect(m.x + 26, m.y + 20, 6, 14);
ctx.fillRect(m.x + 10, m.y + 32, 6, 18);
ctx.fillRect(m.x + 18, m.y + 32, 6, 18);
// Лук в руке
ctx.save();
ctx.translate(m.x + 30, m.y + 22);
ctx.strokeStyle = '#8B4513';
@ -531,17 +653,76 @@ export function loop(now) {
ctx.lineTo(8 * Math.cos(Math.PI * 0.7), 8 * Math.sin(Math.PI * 0.7));
ctx.stroke();
ctx.restore();
} else if (m.kind === 'scorpion') {
// Scorpion: dark brown body, claws, tail
ctx.fillStyle = '#5D4037';
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = '#8D6E63';
ctx.fillRect(m.x + 2, m.y + 10, 6, 8); // left claw
ctx.fillRect(m.x + m.w - 8, m.y + 10, 6, 8); // right claw
// Tail
ctx.fillStyle = '#5D4037';
ctx.beginPath();
ctx.moveTo(m.x + m.w / 2, m.y);
ctx.lineTo(m.x + m.w / 2 - 3, m.y - 10);
ctx.lineTo(m.x + m.w / 2 + 3, m.y - 10);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#FF5722';
ctx.fillRect(m.x + m.w / 2 - 2, m.y - 12, 4, 4); // stinger
} else if (m.kind === 'polar_bear') {
// Polar bear: white body, black nose
ctx.fillStyle = '#ECEFF1';
ctx.fillRect(m.x, m.y, m.w, m.h);
ctx.fillStyle = '#263238';
ctx.fillRect(m.x + m.w / 2 - 2, m.y + 6, 4, 3); // nose
ctx.fillRect(m.x + 8, m.y + 12, 3, 3); // eye
ctx.fillRect(m.x + m.w - 11, m.y + 12, 3, 3); // eye
// legs
ctx.fillStyle = '#CFD8DC';
ctx.fillRect(m.x + 6, m.y + m.h - 8, 8, 8);
ctx.fillRect(m.x + m.w - 14, m.y + m.h - 8, 8, 8);
} else if (m.kind === 'slime') {
// Slime: green bouncy blob
ctx.fillStyle = '#4CAF50';
ctx.beginPath();
ctx.arc(m.x + m.w / 2, m.y + m.h / 2, m.w / 2, 0, Math.PI * 2);
ctx.fill();
// Eyes
ctx.fillStyle = '#fff';
ctx.fillRect(m.x + 6, m.y + 7, 5, 5);
ctx.fillRect(m.x + m.w - 11, m.y + 7, 5, 5);
ctx.fillStyle = '#000';
ctx.fillRect(m.x + 8, m.y + 9, 2, 2);
ctx.fillRect(m.x + m.w - 9, m.y + 9, 2, 2);
} else if (m.kind === 'eagle') {
// Eagle: brown body, wings, yellow beak
ctx.fillStyle = '#5D4037';
ctx.fillRect(m.x + m.w / 2 - 4, m.y + 6, 8, 14); // body
// Wings
ctx.fillStyle = '#795548';
ctx.fillRect(m.x, m.y + 8, m.w / 2 - 4, 6);
ctx.fillRect(m.x + m.w / 2 + 4, m.y + 8, m.w / 2 - 4, 6);
// Head
ctx.fillStyle = '#ECEFF1';
ctx.fillRect(m.x + m.w / 2 - 3, m.y + 2, 6, 6);
// Beak
ctx.fillStyle = '#FFC107';
ctx.fillRect(m.x + m.w / 2 + 3, m.y + 4, 4, 2);
// Eye
ctx.fillStyle = '#000';
ctx.fillRect(m.x + m.w / 2 - 1, m.y + 4, 2, 2);
}
}
// boat (рисуем первой, чтобы игрок был внутри неё)
// boat
if (boat.active) {
ctx.drawImage(tex['boat'], boat.x - (TILE - boat.w) / 2, boat.y - (TILE - boat.h) / 2, TILE, TILE);
}
// other players (multiplayer)
// other players
for (const [socketId, p] of state.otherPlayers) {
if (state.heroImg.complete) {
if (state.heroImg?.complete) {
ctx.drawImage(state.heroImg, p.x - (TILE - player.w) / 2, p.y - (TILE - player.h) / 2, TILE, TILE);
} else {
ctx.fillStyle = p.color;
@ -554,14 +735,14 @@ export function loop(now) {
}
// player
if (state.heroImg.complete) {
if (state.heroImg?.complete) {
ctx.drawImage(state.heroImg, player.x - (TILE - player.w) / 2, player.y - (TILE - player.h) / 2, TILE, TILE);
} else {
ctx.fillStyle = '#fff';
ctx.fillRect(player.x, player.y, player.w, player.h);
}
// projectiles (стрелы)
// projectiles
for (const p of projectiles) {
const angle = Math.atan2(p.vy, p.vx);
ctx.save();
@ -629,7 +810,7 @@ export function loop(now) {
ctx.restore();
// lighting overlay at night (Minecraft-style: offscreen lightmap, wall occlusion, warm flicker)
// lighting overlay at night
if (night) {
const lightC = state.lightC;
const lightCtx = state.lightCtx;
@ -643,16 +824,16 @@ export function loop(now) {
lightCtx.globalCompositeOperation = 'destination-out';
function castLight(sx, sy, radius) {
const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06;
const flick = 0.97 + Math.sin(now / 300 + sx * 0.01) * 0.015 + Math.sin(now / 470 + sy * 0.02) * 0.015;
const r = radius * flick;
const steps2 = 12;
const steps2 = 24;
const dists = new Float32Array(steps2);
for (let i = 0; i < steps2; i++) {
const ang = (i / steps2) * Math.PI * 2;
const ddx = Math.cos(ang);
const ddy = Math.sin(ang);
let maxDist = r;
for (let step = TILE * 0.5; step < r; step += TILE * 0.6) {
for (let step = TILE * 0.3; step < r; step += TILE * 0.35) {
const lgx = Math.floor((sx + ddx * step) / TILE);
const lgy = Math.floor((sy + ddy * step) / TILE);
const blk = getBlock(lgx, lgy);
@ -702,13 +883,13 @@ export function loop(now) {
if (b.gx < minGX - 3 || b.gx > maxGX + 3 || b.gy < minGY - 3 || b.gy > maxGY + 3) continue;
const def = BLOCKS[b.t];
if (def.lightRadius) {
const flick = 0.7 + Math.sin(now / 90 + b.gx * 3.7) * 0.15 + Math.sin(now / 140 + b.gy * 2.3) * 0.15;
const flick = 0.9 + Math.sin(now / 350 + b.gx * 3.7) * 0.05 + Math.sin(now / 530 + b.gy * 2.3) * 0.05;
const wx = b.gx * TILE + TILE / 2 - camX;
const wy = b.gy * TILE + TILE / 2 - camY;
const r = def.lightRadius * 0.6 * flick;
const r = def.lightRadius * 0.85;
const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r);
grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`);
grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`);
grad.addColorStop(0, `rgba(255,180,80,0.28)`);
grad.addColorStop(0.5, `rgba(255,140,40,0.13)`);
grad.addColorStop(1, 'rgba(255,100,20,0)');
ctx.fillStyle = grad;
ctx.beginPath();
@ -719,12 +900,21 @@ export function loop(now) {
ctx.restore();
}
// Дождь (после ночного оверлея)
drawRain();
// Дождь
drawWeather(ctx);
if (Math.random() < 0.25) {
state.hpEl.textContent = Math.max(0, Math.ceil(player.hp));
state.foodEl.textContent = Math.ceil(player.hunger);
document.getElementById('o2').textContent = Math.ceil(player.o2);
// Temperature display
const tempEl = document.getElementById('temp');
if (tempEl) {
const t = Math.round(player.temperature);
tempEl.textContent = t + '°C';
if (player.temperature < COLD_THRESHOLD) tempEl.style.color = '#74b9ff';
else if (player.temperature > HEAT_THRESHOLD) tempEl.style.color = '#e17055';
else tempEl.style.color = '#55efc4';
}
state.sxEl.textContent = Math.floor(player.x / TILE);
state.syEl.textContent = Math.floor(player.y / TILE);
state.todEl.textContent = night ? 'Ночь' : 'День';
@ -749,7 +939,7 @@ export function loop(now) {
ctx.fillText('Нажмите на кровать чтобы проснуться', W / 2, H / 2 + 40);
}
// Миникарта (обновляем раз в ~4 кадра для оптимизации)
// Миникарта
if (state.minimapOpen && Math.random() < 0.25) {
renderMinimap();
}

View File

@ -3,8 +3,9 @@ import { state } from '../core/state.js';
import { getBlock, setBlock, removeBlock } from '../world/world-storage.js';
import { regenerateVisibleChunks } from '../world/generation.js';
import { rebuildHotbar } from '../ui/hotbar.js';
import { renderInventory } from '../ui/inventory.js';
const SAVE_KEY = state.SAVE_KEY;
const SAVE_STORAGE_KEY = state.SAVE_KEY;
let db = null; // Оставляем для совместимости, но не используем
// Инициализация (localStorage + in-memory fallback)
@ -29,6 +30,7 @@ export function saveGame() {
inventory: state.inv,
time: state.worldTime,
isNight: state.isNightTime,
dayNightCycle: state.dayNightCycle,
// Сохраняем только изменения
placedBlocks: state.placedBlocks.slice(),
removedBlocks: state.removedBlocks.slice()
@ -39,7 +41,7 @@ export function saveGame() {
// Пробуем сохранить в localStorage (основной метод для персистентности)
try {
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(saveData));
console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`);
} catch (e) {
console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e);
@ -54,7 +56,7 @@ export function loadGame() {
return new Promise((resolve, reject) => {
// Пробуем localStorage
try {
const localSave = localStorage.getItem(SAVE_KEY);
const localSave = localStorage.getItem(SAVE_STORAGE_KEY);
if (localSave) {
const parsed = JSON.parse(localSave);
console.log('Загружено из localStorage, player HP:', parsed.player?.hp);
@ -161,6 +163,9 @@ export async function applySave(saveData) {
if (saveData.isNight !== undefined) {
state.isNightTime = saveData.isNight;
}
if (saveData.dayNightCycle !== undefined) {
state.dayNightCycle = saveData.dayNightCycle;
}
// Перегенерируем мир по seed
regenerateVisibleChunks();
@ -182,6 +187,11 @@ export async function applySave(saveData) {
state.removedBlocks = saveData.removedBlocks || [];
}
// Fill recentItems from inventory if empty (hotbar won.t render otherwise)
if (state.recentItems.length === 0) {
state.recentItems = Object.keys(state.inv).filter(id => state.inv[id] > 0).slice(0, 5);
}
rebuildHotbar();
renderInventory();
console.log('Игра загружена');
}

View File

@ -3,19 +3,52 @@ import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { getBestTool, useTool, toolTierLessThan, getMiningSpeed } from '../data/tools.js';
import { getBlock, setBlock, removeBlock } from '../world/world-storage.js';
import { playSound } from '../audio/sound-engine.js';
import { rebuildHotbar } from '../ui/hotbar.js';
import { rebuildHotbar, addToRecentItems } from '../ui/hotbar.js';
import { mode } from '../game/modes.js';
import { openFurnaceUI } from '../ui/furnace.js';
import { saveGame } from '../game/save.js';
import { useTool } from '../data/tools.js';
import { activateTNT } from '../world/tnt.js';
import { sendBlockChange } from '../multiplayer/socket-helpers.js';
import { isNight } from '../entities/mob-ai.js';
// Crop stage to seed/harvest mapping
const CROP_SEED_MAP = {
wheat_stage0: 'wheat_seeds', wheat_stage1: 'wheat_seeds', wheat_stage2: 'wheat_seeds', wheat_stage3: 'wheat_seeds',
carrot_stage0: 'carrot', carrot_stage1: 'carrot', carrot_stage2: 'carrot', carrot_stage3: 'carrot',
potato_stage0: 'potato', potato_stage1: 'potato', potato_stage2: 'potato', potato_stage3: 'potato'
};
const CROP_HARVEST_MAP = { wheat_stage3: 'wheat', carrot_stage3: 'carrot', potato_stage3: 'potato' };
// Helper: handle block drops including farming items
function handleBlockDrop(removed, inv) {
// Grass drops wheat_seeds (50%)
if (removed.t === 'grass') {
if (Math.random() < 0.5) inv.wheat_seeds = (inv.wheat_seeds || 0) + 1;
return;
}
// Crop drops: seeds for stages 0-2, harvest+seed for stage 3
const harvestItem = CROP_HARVEST_MAP[removed.t];
const seedItem = CROP_SEED_MAP[removed.t];
if (harvestItem) {
inv[harvestItem] = (inv[harvestItem] || 0) + (BLOCKS[removed.t].harvestQty || 1); addToRecentItems(harvestItem);
if (seedItem) { inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); }
} else if (seedItem) {
// Early crop stage — return seed
inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem);
} else {
inv[removed.t] = (inv[removed.t] || 0) + 1; addToRecentItems(removed.t);
}
}
export const mouse = { x: null, y: null };
// Hold-to-mine tracking
let miningHeld = false;
let miningInterval = null;
export function initMouseHandlers() {
const canvas = state.canvas;
@ -125,7 +158,7 @@ export function initMouseHandlers() {
if (ITEMS[state.selected] && state.inv[state.selected] > 0) {
const it = ITEMS[state.selected];
if (state.player.hp < 100 || state.player.hunger < 100) {
playSound('eat1'); // Звук употребления еды
playSound('eat1');
state.player.hunger = Math.min(100, state.player.hunger + it.food);
state.player.hp = Math.min(100, state.player.hp + 15);
state.inv[state.selected]--;
@ -136,7 +169,7 @@ export function initMouseHandlers() {
// жарка на костре: выбран meat + клик по campfire
if (b && b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) {
playSound('fire'); // Звук при жарке на костре
playSound('fire');
state.inv.meat--;
state.inv.cooked++;
rebuildHotbar();
@ -146,74 +179,209 @@ export function initMouseHandlers() {
// Сон на кровати: клик по bed
if (b && b.t === 'bed' && isNight()) {
state.player.sleeping = true;
saveGame(); // Сохраняем при отходе ко сну
saveGame();
return;
}
// ====== MINING PROGRESSION SYSTEM ======
if (mode() === 'mine') {
if (!b) return;
if (BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return;
// Allow mining crops (growthStage) and harvestable blocks, block other decor and fluids
if (BLOCKS[b.t].fluid || (BLOCKS[b.t].decor && BLOCKS[b.t].growthStage === undefined && !BLOCKS[b.t].harvestable)) return;
if (b.t === 'tnt') { activateTNT(b, 3.2); return; } // не взрывается сразу
if (b.t === 'tnt') { activateTNT(b, 3.2); return; }
const removed = removeBlock(gx, gy);
if (removed) {
state.inv[removed.t] = (state.inv[removed.t] || 0) + 1;
// Calculate mining progress
const hardness = BLOCKS[b.t].hardness || 1;
if (hardness === Infinity) return; // unbreakable (bedrock)
if (hardness === 0) {
// Instant-mine blocks (flower, boat, crops, etc.)
const removed = removeBlock(gx, gy);
if (removed) {
handleBlockDrop(removed, state.inv);
sendBlockChange(gx, gy, removed.t, 'remove');
rebuildHotbar();
}
return;
}
// Тратим прочность кирки (если есть в инвентаре)
const pickTypes = ['iron_pickaxe', 'stone_pickaxe', 'wood_pickaxe'];
for (const pt of pickTypes) {
if (state.inv[pt] > 0) {
const broke = useTool(pt);
if (broke) playSound('cloth1'); // звук поломки
break;
// Same block being mined?
if (state.mining.gx === gx && state.mining.gy === gy) {
state.mining.progress += getMiningSpeed(b.t);
} else {
// New block, start mining
state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness };
}
// Play mining hit sound
if (b.t === 'glass') playSound('glass1');
else if (b.t === 'sand') playSound('sand1');
else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1');
else if (b.t === 'wood' || b.t === 'planks') playSound('wood1');
else playSound('cloth1');
// Check if block is fully mined
if (state.mining.progress >= hardness) {
// Check drop requirements
const minTool = BLOCKS[b.t].minTool;
let drops = true;
if (minTool) {
const toolId = getBestTool(b.t);
if (toolId) {
const tool = TOOLS[toolId];
if (toolTierLessThan(tool.toolTier, minTool)) drops = false;
else useTool(toolId);
} else {
drops = false;
}
} else {
// Use tool durability if applicable
const toolId = getBestTool(b.t);
if (toolId) useTool(toolId);
}
// Отправляем изменение блока на сервер
sendBlockChange(gx, gy, removed.t, 'remove');
const removed = removeBlock(gx, gy);
if (removed) {
if (drops) {
handleBlockDrop(removed, state.inv);
} else {
// Drop cobblestone instead for stone-type blocks
if (BLOCKS[removed.t]?.toolType === 'pickaxe') {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
// Else nothing drops
}
sendBlockChange(gx, gy, removed.t, 'remove');
// Звуки при добыче блоков
if (removed.t === 'glass') playSound('glass1');
else if (removed.t === 'sand') playSound('sand1');
else if (removed.t === 'snow') playSound('snow1');
else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1');
else if (removed.t === 'wood') playSound('wood1');
else playSound('cloth1');
// Sound for block break
if (removed.t === 'glass') playSound('glass1');
else if (removed.t === 'sand') playSound('sand1');
else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1');
else if (removed.t === 'wood') playSound('wood1');
else playSound('cloth1');
rebuildHotbar();
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
}
// Start hold-to-mine for desktop
miningHeld = true;
if (miningInterval) clearInterval(miningInterval);
miningInterval = setInterval(() => {
if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; }
// Re-check the block still exists at mining position
const mb = getBlock(state.mining.gx, state.mining.gy);
if (!mb || mb.dead || mb.t !== b.t) {
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
return;
}
// Accumulate progress
if (state.mining.gx !== null && state.mining.hardness > 0) {
state.mining.progress += getMiningSpeed(mb.t) * 0.3; // 0.3x per tick for held mining
if (state.mining.progress >= state.mining.hardness) {
const mgx = state.mining.gx;
const mgy = state.mining.gy;
const mMinTool = BLOCKS[mb.t].minTool;
let mDrops = true;
if (mMinTool) {
const mToolId = getBestTool(mb.t);
if (mToolId) {
const mTool = TOOLS[mToolId];
if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false;
else useTool(mToolId);
} else {
mDrops = false;
}
} else {
const mToolId = getBestTool(mb.t);
if (mToolId) useTool(mToolId);
}
const removed = removeBlock(mgx, mgy);
if (removed) {
if (mDrops) {
handleBlockDrop(removed, state.inv);
} else {
if (BLOCKS[removed.t]?.toolType === 'pickaxe') {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
}
sendBlockChange(mgx, mgy, removed.t, 'remove');
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
}
}
}, 100);
return;
}
// ====== BUILD MODE ======
if (mode() === 'build') {
// Planting seeds on farmland (works even without farmland in inventory)
const seedToCropBlock = {
wheat_seeds: 'wheat_stage0',
carrot: 'carrot_stage0',
potato: 'potato_stage0'
};
if (seedToCropBlock[state.selected] && state.inv[state.selected] > 0) {
// Clicking on farmland — plant above it
if (b && b.t === 'farmland') {
const aboveKey = gx + ',' + (gy - 1);
const above = state.grid.get(aboveKey);
if (!above || above.dead) {
setBlock(gx, gy - 1, seedToCropBlock[state.selected], true);
state.inv[state.selected]--;
const cropBlock = state.grid.get(gx + ',' + (gy - 1));
if (cropBlock) cropBlock.growthTimer = 0;
rebuildHotbar();
return;
}
}
// Clicking on empty space above farmland — plant there
if (!b) {
const belowKey = gx + ',' + (gy + 1);
const below = state.grid.get(belowKey);
if (below && below.t === 'farmland') {
setBlock(gx, gy, seedToCropBlock[state.selected], true);
state.inv[state.selected]--;
const cropBlock = state.grid.get(gx + ',' + gy);
if (cropBlock) cropBlock.growthTimer = 0;
rebuildHotbar();
return;
}
}
}
if (state.inv[state.selected] <= 0) return;
if (!BLOCKS[state.selected]) return;
if (b) return; // занято
// Проверяем, ставим ли лодку
if (state.selected === 'boat') {
// Лодку можно ставить только на воду
const waterBelow = getBlock(gx, gy + 1);
if (!waterBelow || waterBelow.t !== 'water') {
return;
}
// Создаём лодку
state.boat.x = gx * state.TILE;
state.boat.y = gy * state.TILE;
state.boat.vx = 0;
state.boat.vy = 0;
state.boat.active = true;
state.boat.inWater = true;
// Сажаем игрока в лодку
state.player.inBoat = true;
state.player.x = state.boat.x;
state.player.y = state.boat.y;
state.player.vx = 0;
state.player.vy = 0;
playSound('splash');
state.inv[state.selected]--;
rebuildHotbar();
@ -226,22 +394,29 @@ export function initMouseHandlers() {
const overlap = !(bx >= state.player.x + state.player.w || bx + TILE <= state.player.x || by >= state.player.y + state.player.h || by + TILE <= state.player.y);
if (overlap) return;
setBlock(gx, gy, state.selected, true); // true = блок установлен игроком
setBlock(gx, gy, state.selected, true);
state.inv[state.selected]--;
// Отправляем изменение блока на сервер
sendBlockChange(gx, gy, state.selected, 'set');
// Звук при строительстве
if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build');
else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build');
else if (state.selected === 'glass') playSound('glass1');
else if (state.selected === 'sand') playSound('sand1');
else if (state.selected === 'snow') playSound('snow1');
else if (state.selected === 'dirt' || state.selected === 'grass') playSound('cloth1');
rebuildHotbar();
return;
}
});
// Release mouse — stop hold-to-mine
canvas.addEventListener('pointerup', () => {
miningHeld = false;
});
canvas.addEventListener('pointerleave', () => {
miningHeld = false;
// Reset mining when mouse leaves canvas
});
}

View File

@ -1,278 +1,358 @@
// ==================== ENTRY POINT ====================
import { state } from './core/state.js';
import { SERVER_URL } from './config.js';
import { showStartMenu, hideBoot, isTgWebApp, tgUser } from './ui/start-menu.js';
import { initCanvas } from './core/canvas.js';
import { initSocket } from './multiplayer/socket.js';
import { sendBlockChange, sendPlayerPosition } from './multiplayer/socket-helpers.js';
import { loadSound, playSound } from './audio/sound-engine.js';
import { BLOCKS } from './data/blocks.js';
import { ITEMS } from './data/items.js';
import { TOOLS } from './data/tools.js';
import { SMELTING_RECIPES } from './data/recipes.js';
import { initTextures } from './render/textures.js';
import { initTextures, tex } from './render/textures.js';
import { getBlock, setBlock, removeBlock } from './world/world-storage.js';
import { genColumn, surfaceGyAt, ensureGenAroundCamera, regenerateVisibleChunks } from './world/generation.js';
import { genColumn, surfaceGyAt } from './world/generation.js';
import { initDB, loadGame, applySave, saveGame } from './game/save.js';
import { loop } from './game/loop.js';
import { initChat } from './ui/chat.js';
import { initFurnace } from './ui/furnace.js';
import { initMinimap } from './ui/minimap.js';
import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js';
import { initMarket } from './ui/market.js';
import { initSettings } from './ui/settings.js';
import { initRespawn } from './ui/respawn.js';
import { initShare } from './ui/share.js';
import { initControls } from './input/controls.js';
import { initMouseHandlers } from './input/mouse-handler.js';
import { initModes } from './game/modes.js';
import { initVoice } from './multiplayer/voice-chat.js';
import { resolveY, resolveX } from './physics/collision.js';
import { calculateDamage } from './entities/player.js';
import { updateWaterFlag } from './physics/water-detect.js';
import { updateWaterPhysics } from './physics/water.js';
import { explodeAt, activateTNT } from './world/tnt.js';
import { useTool } from './data/tools.js';
import { rebuildHotbar } from './ui/hotbar.js';
import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js';
// ==================== 1) CONFIG — parse URL/getName ====================
const urlParams = new URLSearchParams(window.location.search);
state.SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru';
// ==================== 1) CONFIG — parse URL ====================
// urlParams defined in config.js
state.SERVER_URL = SERVER_URL;
state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot';
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
// Защита от mixed content
if (location.protocol === 'https:' && state.SERVER_URL.startsWith('http://')) {
console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP');
alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.');
// Set TG-related state
if (isTgWebApp) {
state.myTgId = tgUser?.id || null;
}
// Player name
state.playerName = localStorage.getItem('minegrechka_playerName') || null;
if (!state.playerName) {
state.playerName = prompt('Введите ваше имя для игры:') || 'Игрок';
localStorage.setItem('minegrechka_playerName', state.playerName);
console.log('Player name set:', state.playerName);
}
state.heroImg = new Image(); state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
// World ID from URL or generate new
console.log('Current URL:', window.location.href);
const worldParam = urlParams.get('world');
console.log('world param:', worldParam);
// ==================== 2) START MENU ====================
// Show the start menu before initializing the game.
// The menu returns a Promise that resolves with the worldId.
async function startGame() {
const worldId = await showStartMenu();
// Set worldId and playerName
state.worldId = worldId;
if (!state.playerName) {
state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок';
localStorage.setItem('minegrechka_playerName', state.playerName);
}
console.log('[Launch] worldId:', state.worldId, 'player:', state.playerName, 'tg:', isTgWebApp);
state.worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null;
console.log('worldId after params:', state.worldId, 'type:', typeof state.worldId);
if (!state.worldId) {
state.worldId = Math.random().toString(36).substring(2, 10);
console.log('Generated worldId:', state.worldId);
// Validate TG initData if in TG context
if (isTgWebApp && window.Telegram && Telegram.WebApp.initData) {
try {
const resp = await fetch(state.SERVER_URL + '/api/tg/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData: Telegram.WebApp.initData })
});
const data = await resp.json();
if (data.ok && data.tg_id) {
console.log('[TG] Authenticated, tg_id:', data.tg_id);
state.myTgId = data.tg_id;
}
} catch (e) { console.warn('[TG] Auth failed:', e); }
}
// Update URL with world code
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', state.worldId);
const newUrlString = newUrl.toString();
console.log('New URL to set:', newUrlString);
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', newUrlString);
console.log('URL after replaceState:', window.location.href);
} else {
console.error('History API not supported!');
if (typeof history !== 'undefined' && history.replaceState) {
history.replaceState(null, '', newUrl.toString());
}
} catch (e) {
console.error('Error updating URL:', e);
} catch (e) {}
// ==================== 3) CANVAS — init from canvas module ====================
initCanvas();
// ==================== 4) STATE — set DOM refs ====================
state.hpEl = document.getElementById('hp');
state.foodEl = document.getElementById('food');
state.sxEl = document.getElementById('sx');
state.syEl = document.getElementById('sy');
state.todEl = document.getElementById('tod');
state.worldIdEl = document.getElementById('worldId');
state.playerCountEl = document.getElementById('playerCount');
state.deathEl = document.getElementById('death');
state.hotbarEl = document.getElementById('hotbar');
state.craftPanel = document.getElementById('craftPanel');
state.recipesEl = document.getElementById('recipes');
state.inventoryPanel = document.getElementById('inventoryPanel');
state.inventoryGrid = document.getElementById('inventoryGrid');
state.marketPanel = document.getElementById('marketPanel');
state.marketContent = document.getElementById('marketContent');
state.tradePanel = document.getElementById('tradePanel');
state.tradeContent = document.getElementById('tradeContent');
state.multiplayerStatus = document.getElementById('multiplayerStatus');
// ==================== 5) TEXTURES — initTextures() ====================
initTextures();
state.tex = tex;
// ==================== 6) CRAFT/INVENTORY BUTTONS ====================
const craftBtn = document.getElementById('craftBtn');
const invToggle = document.getElementById('invToggle');
const craftClose = document.getElementById('craftClose');
const inventoryClose = document.getElementById('inventoryClose');
if (craftBtn) craftBtn.onclick = () => toggleCraft();
if (craftClose) craftClose.onclick = () => closeCraft();
if (invToggle) invToggle.onclick = () => toggleInventory();
if (inventoryClose) inventoryClose.onclick = () => closeInventory();
// ==================== 7) CONTROLS — initControls ====================
initControls();
// ==================== 8) MOUSE — initMouseHandlers ====================
initMouseHandlers();
// ==================== 9) UI MODULES — init all UI ====================
initChat();
initFurnace();
initMinimap();
initSaveControls();
initMarket();
initRespawn();
initShare();
initSettings();
initModes();
// ==================== 10) SOCKET — initSocket ====================
initSocket();
// ==================== 11) VOICE — initVoice ====================
initVoice();
// ==================== 12) SAVE — loadGame, applySave ====================
// Ensure hotbar has items on first load
if (state.recentItems.length === 0) {
state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5);
}
// Make sure selected item is valid and in recentItems
if (!state.selected || !state.inv[state.selected] || state.inv[state.selected] <= 0) {
state.selected = state.recentItems[0] || 'dirt';
}
if (!state.recentItems.includes(state.selected)) {
state.recentItems.unshift(state.selected);
state.recentItems = state.recentItems.slice(0, 5);
}
rebuildHotbar();
console.log('Generated new worldId for browser:', state.worldId);
}
// Keyboard hotbar switching (1/2/3)
document.addEventListener('keydown', (e) => {
if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return;
if (e.key >= '1' && e.key <= '3') {
const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3);
const idx = parseInt(e.key) - 1;
if (items[idx]) {
state.selected = items[idx];
rebuildHotbar();
}
}
});
console.log('Final worldId:', state.worldId, 'Player name:', state.playerName);
// Scroll wheel hotbar cycling
document.addEventListener('wheel', (e) => {
if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return;
const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3);
if (items.length === 0) return;
let idx = items.indexOf(state.selected);
if (idx < 0) idx = 0;
if (e.deltaY > 0) idx = (idx + 1) % items.length;
else idx = (idx - 1 + items.length) % items.length;
state.selected = items[idx];
rebuildHotbar();
e.preventDefault();
}, { passive: false });
// ==================== 2) CANVAS — get elements, resize ====================
state.gameEl = document.getElementById('game');
state.canvas = document.getElementById('c');
state.ctx = state.canvas.getContext('2d');
initDB().then(async () => {
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log('Загружено сохранение, HP:', state.player.hp);
// offscreen light map
state.lightC = document.createElement('canvas');
state.lightCtx = state.lightC.getContext('2d');
if (state.player.hp <= 0) {
console.log('WARNING: HP <= 0 после загрузки, возрождаемся');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
state.player.fallStartY = state.player.y;
}
} else {
console.log('Сохранение не найдено, начинаем новую игру');
// Fill recentItems from inventory
state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5);
rebuildHotbar();
state.dpr = Math.max(1, window.devicePixelRatio || 1);
function resize() {
state.W = state.gameEl.clientWidth;
state.H = state.gameEl.clientHeight;
state.canvas.width = state.W * state.dpr;
state.canvas.height = state.H * state.dpr;
state.lightC.width = state.W * state.dpr;
state.lightC.height = state.H * state.dpr;
state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ==================== 3) STATE — init player, inv, etc ====================
state.otherPlayers = new Map();
state.serverMobs = new Map();
state.grid = new Map();
state.blocks = [];
state.generated = new Set();
state.serverOverrides = new Map();
state.activeFurnaces = new Map();
state.activeTNT = new Set();
state.toolDurability = new Map();
state.worldSeed = Math.floor(Math.random() * 1000000);
// Clouds
state.clouds = Array.from({ length: 10 }, () => ({
x: Math.random() * 2000,
y: -200 - Math.random() * 260,
w: 80 + Math.random() * 120,
s: 12 + Math.random() * 20
}));
// Weather
state.weatherTimer = 0;
state.weatherChangeInterval = 60 + Math.random() * 120;
// Player spawn
state.player.x = 6 * state.TILE;
state.player.y = 0;
state.spawnPoint.x = 6 * state.TILE;
state.spawnPoint.y = 0;
// Hero image
state.heroImg = new Image();
state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
// UI elements
state.hpEl = document.getElementById('hp');
state.foodEl = document.getElementById('food');
state.sxEl = document.getElementById('sx');
state.syEl = document.getElementById('sy');
state.todEl = document.getElementById('tod');
state.worldIdEl = document.getElementById('worldId');
state.playerCountEl = document.getElementById('playerCount');
state.deathEl = document.getElementById('death');
// ==================== 4) TEXTURES — initTextures() ====================
initTextures();
// ==================== 5) AUDIO — loadSound for each ====================
loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3');
loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3');
loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3');
loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3');
loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3');
loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3');
loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3');
loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3');
loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3');
loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3');
loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3');
loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3');
loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3');
loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3');
loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3');
loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3');
loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3');
// ==================== 6) CONTROLS — initControls ====================
initControls();
// ==================== 7) MOUSE — initMouseHandlers ====================
initMouseHandlers();
// ==================== 8) UI MODULES — init all UI ====================
initChat();
initFurnace();
initMinimap();
initSaveControls();
initRespawn();
initShare();
initModes();
// ==================== 9) SOCKET — initSocket ====================
initSocket();
// ==================== 10) VOICE — initVoice ====================
initVoice();
// ==================== 11) SAVE — loadGame, applySave ====================
rebuildHotbar();
initDB().then(async () => {
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log('Загружено сохранение, HP:', state.player.hp);
if (state.player.hp <= 0) {
console.log('WARNING: HP <= 0 после загрузки, возрождаемся');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
// старт — на поверхности
const startGX = 6;
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
const surfaceY = surfaceGyAt(startGX);
let safeGY = surfaceY - 1;
const aboveBlock = getBlock(startGX, surfaceY - 1);
if (aboveBlock && aboveBlock.t === 'water') {
for (let gy = state.SEA_GY - 1; gy >= 0; gy--) {
const b = getBlock(startGX, gy);
if (!b || b.dead || b.t === 'air' || b.t === 'water') continue;
safeGY = gy - 1;
break;
}
}
state.player.y = safeGY * state.TILE;
state.player.x = startGX * state.TILE;
state.player.fallStartY = state.player.y;
}
} else {
console.log('Сохранение не найдено, начинаем новую игру');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
// старт — на поверхности
const startGX = 6;
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
const surfaceY = surfaceGyAt(startGX);
let safeGY = surfaceY - 1;
const aboveBlock = getBlock(startGX, surfaceY - 1);
if (aboveBlock && aboveBlock.t === 'water') {
for (let gy = state.SEA_GY - 1; gy >= 0; gy--) {
const b = getBlock(startGX, gy);
if (!b || b.dead || b.t === 'air' || b.t === 'water') continue;
safeGY = gy - 1;
break;
console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y);
// Генерируем карту вокруг стартовой позиции
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
}
state.player.y = safeGY * state.TILE;
state.player.x = startGX * state.TILE;
// Автосейв при скрытии страницы
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
saveGame();
}
});
// Автосейв перед закрытием страницы
window.addEventListener('beforeunload', () => {
saveGame();
});
}).catch(err => {
console.error('Ошибка инициализации:', err);
const startGX = 6;
genColumn(startGX);
state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
state.player.fallStartY = state.player.y;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y);
// Генерируем карту вокруг стартовой позиции
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
}
});
// Автосейв при скрытии страницы
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
saveGame();
// ==================== 13) LOOP — startLoop ====================
requestAnimationFrame(loop);
// ==================== 14) WORLD-GEN PLACEHOLDER ====================
// 2D-мир генерируется лениво, колонками вокруг игрока. В первые секунды
// на экране только небо и HUD — пользователь считает это поломкой.
// Показываем заглушку, пока реальных блоков не видно, и гасим её при первом
// отрисованном блоке. Счётчики — из generated (Set сгенерированных колонок).
installWorldGenPlaceholder();
}
// ==================== WORLD-GEN PLACEHOLDER ====================
let _wgPh = null;
function installWorldGenPlaceholder() {
if (_wgPh || document.getElementById('wgPh')) return;
hideBoot();
const el = document.createElement('div');
el.id = 'wgPh';
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
el.innerHTML =
'<div class="wgPh-box">' +
'<div class="wgPh-title">Генерация мира</div>' +
'<div class="wgPh-bar"><i></i></div>' +
'<div class="wgPh-hint">Первые секунды мир строится по колонкам</div>' +
'</div>';
document.body.appendChild(el);
_wgPh = el;
// Стили — отдельным <style>, чтобы не трогать style.css и не ломать сборку.
const st = document.createElement('style');
st.textContent = [
'#wgPh{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;',
'justify-content:center;background:linear-gradient(#0f1720,#1c2b3a);',
'font:14px/1.4 system-ui,-apple-system,sans-serif;color:#e8f0f8}',
'#wgPh.gone{display:none}',
'.wgPh-box{text-align:center;max-width:320px;padding:0 24px}',
'.wgPh-title{font-size:18px;margin-bottom:14px}',
'.wgPh-bar{height:6px;background:rgba(255,255,255,.12);border-radius:3px;overflow:hidden}',
'.wgPh-bar>i{display:block;height:100%;width:35%;background:#5a8f3c;border-radius:3px;',
'animation:wgPhSlide 1.1s ease-in-out infinite}',
'@keyframes wgPhSlide{0%{margin-left:-35%}100%{margin-left:100%}}',
'.wgPh-hint{margin-top:12px;color:#8fa3b8;font-size:12px}',
].join('');
document.head.appendChild(st);
// Наблюдаем 60 с: MP-join (world_state) может очистить мир ПОСЛЕ того,
// как заглушка уже погасла — тогда показываем её снова. Убираем из DOM
// только по истечении срока.
const deadline = Date.now() + 60000;
const born = Date.now();
const tick = () => {
if (!_wgPh || !_wgPh.isConnected) return;
if (Date.now() > deadline) {
el.remove();
return;
}
});
// Минимальный показ 600 мс — иначе заглушка мелькает незаметно.
if (!worldHasVisibleBlocks() || Date.now() - born < 600) el.classList.remove('gone');
else el.classList.add('gone');
setTimeout(tick, 200);
};
setTimeout(tick, 150);
}
// Автосейв перед закрытием страницы
window.addEventListener('beforeunload', () => {
saveGame();
});
}).catch(err => {
console.error('Ошибка инициализации:', err);
const startGX = 6;
genColumn(startGX);
state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
state.player.fallStartY = state.player.y;
function worldHasVisibleBlocks() {
try {
const gx = Math.floor((state.player?.x ?? 0) / state.TILE);
for (let dx = -3; dx <= 3; dx++) {
for (let up = 0; up < 30; up++) {
const b = getBlock(gx + dx, up);
if (b && b.t && b.t !== 'air' && b.t !== 'water' && b.t !== 'swamp_water') return true;
}
}
} catch (e) { /* getBlock может быть недоступен на ранних кадрах */ }
return false;
}
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
// Start the game
startGame().catch(err => {
console.error('Fatal error starting game:', err);
});
// ==================== 13) LOOP — startLoop ====================
requestAnimationFrame(loop);
// Debug: give items
window.debugGive = function(id, count) {
state.inv[id] = (state.inv[id] || 0) + (count || 1);
if (!state.recentItems.includes(id)) { state.recentItems.unshift(id); state.recentItems = state.recentItems.slice(0, 5); }
rebuildHotbar();
// Can't call renderInventory from here, but user can click inv
console.log('Gave', count || 1, id, '=>', state.inv[id]);
console.log('inv keys>0:', Object.keys(state.inv).filter(k => state.inv[k] > 0).join(', '));
};
window.debugInv = function() { return Object.keys(state.inv).filter(k => state.inv[k] > 0).map(k => k + ':' + state.inv[k]); };

299
src/main.js.bak.broken Normal file
View File

@ -0,0 +1,299 @@
// ==================== ENTRY POINT ====================
import { state } from './core/state.js';
import { initSocket } from './multiplayer/socket.js';
import { sendBlockChange, sendPlayerPosition } from './multiplayer/socket-helpers.js';
import { loadSound, playSound } from './audio/sound-engine.js';
import { BLOCKS } from './data/blocks.js';
import { ITEMS } from './data/items.js';
import { TOOLS } from './data/tools.js';
import { SMELTING_RECIPES } from './data/recipes.js';
import { initTextures } from './render/textures.js';
import { getBlock, setBlock, removeBlock } from './world/world-storage.js';
import { genColumn, surfaceGyAt, ensureGenAroundCamera, regenerateVisibleChunks } from './world/generation.js';
import { initDB, loadGame, applySave, saveGame } from './game/save.js';
import { loop } from './game/loop.js';
import { initChat } from './ui/chat.js';
import { initFurnace } from './ui/furnace.js';
import { initMinimap } from './ui/minimap.js';
import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js';
import { initRespawn } from './ui/respawn.js';
import { initShare } from './ui/share.js';
import { initControls } from './input/controls.js';
import { initMouseHandlers } from './input/mouse-handler.js';
import { initModes } from './game/modes.js';
import { initVoice } from './multiplayer/voice-chat.js';
import { resolveY, resolveX } from './physics/collision.js';
import { calculateDamage } from './entities/player.js';
import { updateWaterFlag } from './physics/water-detect.js';
import { updateWaterPhysics } from './physics/water.js';
import { explodeAt, activateTNT } from './world/tnt.js';
import { useTool } from './data/tools.js';
import { rebuildHotbar } from './ui/hotbar.js';
import { closeTrade } from './ui/trade.js';
import { toggleMarket, closeMarket } from './ui/market.js';
// ==================== 1) CONFIG — parse URL/getName ====================
const urlParams = new URLSearchParams(window.location.search);
state.SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru';
state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot';
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
// Защита от mixed content
if (location.protocol === 'https:' && state.SERVER_URL.startsWith('http://')) {
console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP');
alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.');
}
// Player name
state.playerName = localStorage.getItem('minegrechka_playerName') || null;
if (!state.playerName) {
state.playerName = prompt('Введите ваше имя для игры:') || 'Игрок';
localStorage.setItem('minegrechka_playerName', state.playerName);
console.log('Player name set:', state.playerName);
}
// World ID from URL or generate new
console.log('Current URL:', window.location.href);
const worldParam = urlParams.get('world');
console.log('world param:', worldParam);
state.worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null;
console.log('worldId after params:', state.worldId, 'type:', typeof state.worldId);
if (!state.worldId) {
state.worldId = Math.random().toString(36).substring(2, 10);
console.log('Generated worldId:', state.worldId);
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', state.worldId);
const newUrlString = newUrl.toString();
console.log('New URL to set:', newUrlString);
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', newUrlString);
console.log('URL after replaceState:', window.location.href);
} else {
console.error('History API not supported!');
}
} catch (e) {
console.error('Error updating URL:', e);
}
console.log('Generated new worldId for browser:', state.worldId);
}
console.log('Final worldId:', state.worldId, 'Player name:', state.playerName);
// ==================== 2) CANVAS — get elements, resize ====================
state.gameEl = document.getElementById('game');
state.canvas = document.getElementById('c');
state.ctx = state.canvas.getContext('2d');
// offscreen light map
state.lightC = document.createElement('canvas');
state.lightCtx = state.lightC.getContext('2d');
state.dpr = Math.max(1, window.devicePixelRatio || 1);
function resize() {
state.W = state.gameEl.clientWidth;
state.H = state.gameEl.clientHeight;
state.canvas.width = state.W * state.dpr;
state.canvas.height = state.H * state.dpr;
state.lightC.width = state.W * state.dpr;
state.lightC.height = state.H * state.dpr;
state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ==================== 3) STATE — init player, inv, etc ====================
state.otherPlayers = new Map();
state.serverMobs = new Map();
state.grid = new Map();
state.blocks = [];
state.generated = new Set();
state.serverOverrides = new Map();
state.activeFurnaces = new Map();
state.activeTNT = new Set();
state.toolDurability = new Map();
state.worldSeed = Math.floor(Math.random() * 1000000);
// Clouds
state.clouds = Array.from({ length: 10 }, () => ({
x: Math.random() * 2000,
y: -200 - Math.random() * 260,
w: 80 + Math.random() * 120,
s: 12 + Math.random() * 20
}));
// Weather
state.weatherTimer = 0;
state.weatherChangeInterval = 60 + Math.random() * 120;
// Player spawn
state.player.x = 6 * state.TILE;
state.player.y = 0;
state.spawnPoint.x = 6 * state.TILE;
state.spawnPoint.y = 0;
// Hero image
state.heroImg = new Image();
state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
// UI elements
state.hpEl = document.getElementById('hp');
state.foodEl = document.getElementById('food');
state.sxEl = document.getElementById('sx');
state.syEl = document.getElementById('sy');
state.todEl = document.getElementById('tod');
state.worldIdEl = document.getElementById('worldId');
state.playerCountEl = document.getElementById('playerCount');
state.deathEl = document.getElementById('death');
state.multiplayerStatus = document.getElementById('multiplayerStatus');
// Market and trade panel DOM refs
state.marketPanel = document.getElementById('marketPanel');
state.marketContent = document.getElementById('marketContent');
state.tradePanel = document.getElementById('tradePanel');
state.tradeContent = document.getElementById('tradeContent');
// ==================== 4) TEXTURES — initTextures() ====================
initTextures();
// ==================== 5) AUDIO — loadSound for each ====================
loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3');
loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3');
loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3');
loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3');
loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3');
loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3');
loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3');
loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3');
loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3');
loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3');
loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3');
loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3');
loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3');
loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3');
loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3');
loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3');
loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3');
// ==================== 6) CONTROLS — initControls ====================
initControls();
// ==================== 7) MOUSE — initMouseHandlers ====================
initMouseHandlers();
// ==================== 8) UI MODULES — init all UI ====================
initChat();
initFurnace();
initMinimap();
initSaveControls();
initRespawn();
initShare();
initModes();
// ==================== MARKET & TRADE UI ====================
// Market close
const marketClose = document.getElementById('marketClose');
if (marketClose) {
marketClose.addEventListener('click', () => {
closeMarket();
});
}
// Trade close
const tradeClose = document.getElementById('tradeClose');
if (tradeClose) {
tradeClose.addEventListener('click', () => {
closeTrade();
});
}
// ==================== 9) SOCKET — initSocket ====================
initSocket();
// ==================== 10) VOICE — initVoice ====================
initVoice();
// ==================== 11) SAVE — loadGame, applySave ====================
rebuildHotbar();
initDB().then(async () => {
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log('Загружено сохранение, HP:', state.player.hp);
if (state.player.hp <= 0) {
console.log('WARNING: HP <= 0 после загрузки, возрождаемся');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
state.player.fallStartY = state.player.y;
}
} else {
console.log('Сохранение не найдено, начинаем новую игру');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
// старт — на поверхности
const startGX = 6;
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
const surfaceY = surfaceGyAt(startGX);
let safeGY = surfaceY - 1;
const aboveBlock = getBlock(startGX, surfaceY - 1);
if (aboveBlock && aboveBlock.t === 'water') {
for (let gy = state.SEA_GY - 1; gy >= 0; gy--) {
const b = getBlock(startGX, gy);
if (!b || b.dead || b.t === 'air' || b.t === 'water') continue;
safeGY = gy - 1;
break;
}
}
state.player.y = safeGY * state.TILE;
state.player.x = startGX * state.TILE;
state.player.fallStartY = state.player.y;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y);
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
}
// Автосейв
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
saveGame();
}
window.addEventListener('beforeunload', () => {
saveGame();
}).catch(err => {
console.error('Ошибка инициализации:', err);
const startGX = 6;
genColumn(startGX);
state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
state.player.fallStartY = state.player.y;
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
});
// ==================== 13) LOOP — startLoop ====================
requestAnimationFrame(loop);

View File

@ -1,6 +1,8 @@
// Socket helpers: цвет, отправка позиции/блоков
import { state } from '../core/state.js';
import { SERVER_URL } from '../config.js';
import { MOVE_SEND_INTERVAL } from '../core/constants.js';
// Генерация случайного цвета для игрока на основе socket_id
export function getRandomPlayerColor(socketId) {
@ -15,7 +17,7 @@ export function getRandomPlayerColor(socketId) {
// Throttle для отправки позиции (10-20 раз в секунду)
let lastMoveSendTime = 0;
const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду
let lastSentX = 0, lastSentY = 0;
// Отправка позиции игрока (с throttle)

View File

@ -1,9 +1,9 @@
// Socket.IO клиент — инициализация и все обработчики
import { state } from '../core/state.js';
import { SERVER_URL, worldId, playerName } from '../config.js';
import { SERVER_URL } from '../config.js';
import { TILE, SEA_GY } from '../core/constants.js';
import { BLOCKS } from '../data/blocks.js';
import { k, setBlock, removeBlock, grid, blocks } from '../world/world-storage.js';
import { k, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage.js';
import { genColumn, surfaceGyAt } from '../world/generation.js';
import { addChatMessage } from '../ui/chat.js';
import { calculateDamage } from '../entities/player.js';
@ -12,6 +12,25 @@ import { rebuildHotbar } from '../ui/hotbar.js';
import { explodeAt } from '../world/tnt.js';
import { updateSaveButtonVisibility } from '../ui/save-controls.js';
import { getRandomPlayerColor, sendPlayerPosition, sendBlockChange } from './socket-helpers.js';
import { initTradeSocket } from '../ui/trade.js';
// Mob sizes for server mob sync (server doesn't send w/h)
const MOB_SIZES = {
pig: { w: 34, h: 24 },
chicken: { w: 26, h: 22 },
zombie: { w: 34, h: 50 },
creeper: { w: 34, h: 50 },
skeleton: { w: 34, h: 50 },
scorpion: { w: 26, h: 26 },
polar_bear: { w: 40, h: 34 },
slime: { w: 24, h: 24 },
eagle: { w: 30, h: 22 },
};
function applyMobSize(sm) {
const s = MOB_SIZES[sm.kind];
if (s && !sm.w) { sm.w = s.w; sm.h = s.h; }
return sm;
}
let socket = null;
@ -24,17 +43,23 @@ export function initSocket() {
transports: ['websocket', 'polling']
});
// Store socket on state for other modules
state.socket = socket;
socket.on('connect', () => {
console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`);
console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`);
state.mySocketId = socket.id;
state.isMultiplayer = true;
// Присоединяемся к миру
socket.emit('join_world', { world_id: worldId, player_name: playerName });
socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName });
// Показываем в UI
state.worldIdEl.textContent = worldId;
state.multiplayerStatus.style.display = 'block';
if (state.worldIdEl) state.worldIdEl.textContent = state.worldId;
if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block';
// Initialize trade socket handlers
initTradeSocket(socket);
});
socket.on('connect_error', (error) => {
@ -46,20 +71,18 @@ export function initSocket() {
console.log('Disconnected from server');
state.isMultiplayer = false;
state.otherPlayers.clear();
state.multiplayerStatus.style.display = 'none';
if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none';
});
// Обработка world_state
socket.on('world_state', (data) => {
console.log('Received world_state:', data);
// Устанавливаем seed и перегенерируем мир если он изменился
if (data.seed !== undefined && data.seed !== state.worldSeed) {
const oldSeed = state.worldSeed;
state.worldSeed = data.seed;
console.log('World seed changed from', oldSeed, 'to', state.worldSeed);
// Очищаем и перегенерируем мир с новым seed
state.generated.clear();
grid.clear();
blocks.length = 0;
@ -68,12 +91,10 @@ export function initSocket() {
console.log('World regenerated with new seed:', state.worldSeed);
}
// Применяем блоки — сохраняем в state.serverOverrides для применения после genColumn
if (data.blocks && Array.isArray(data.blocks)) {
for (const block of data.blocks) {
const key = k(block.gx, block.gy);
state.serverOverrides.set(key, { op: block.op, t: block.t });
// Также пробуем применить сразу (если колонна уже сгенерирована)
if (block.op === 'set') {
setBlock(block.gx, block.gy, block.t, false);
} else if (block.op === 'remove') {
@ -82,25 +103,19 @@ export function initSocket() {
}
}
// Устанавливаем время
if (data.time !== undefined) {
state.worldTime = data.time;
state.isNightTime = state.worldTime > 0.5;
}
// Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt
// Это гарантирует совпадение с terrain generation
// Spawn point
{
const startGX = 6;
// Генерируем колонну и соседние для безопасного спавна
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
const surfaceY = surfaceGyAt(startGX);
// Ищем ближайшую небудущую позицию сверху вниз от поверхности
let safeGY = surfaceY - 1;
// Проверяем что над поверхностью воздух (не в воде)
const aboveBlock = getBlock(startGX, surfaceY - 1);
if (aboveBlock && aboveBlock.t === 'water') {
// Если в воде — ищем поверхность выше уровня моря
for (let gy = SEA_GY - 1; gy >= 0; gy--) {
const b = getBlock(startGX, gy);
if (!b || b.dead || b.t === 'air' || b.t === 'water') continue;
@ -113,7 +128,6 @@ export function initSocket() {
console.log('Client-side spawn point:', state.spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY);
}
// Устанавливаем игрока в точку спавна
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = 0;
@ -121,14 +135,12 @@ export function initSocket() {
state.player.fallStartY = state.player.y;
console.log('Player moved to spawn point:', state.player.x, state.player.y);
// Устанавливаем HP на 100% при каждом подключении к миру
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.invuln = 0;
console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect');
// Обновляем список игроков
if (data.players && Array.isArray(data.players)) {
state.otherPlayers.clear();
for (const p of data.players) {
@ -141,24 +153,25 @@ export function initSocket() {
});
}
}
// Обновляем счётчик игроков
state.playerCountEl.textContent = data.players.length;
}
// Server mobs
if (data.mobs && Array.isArray(data.mobs)) {
state.serverMobs.clear();
for (const m of data.mobs) {
const sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx || 0, vy: m.vy || 0, grounded: false, inWater: false, aiT: 0, dir: m.dir || 1, dead: false, fuse: m.fuse || 0, shootCooldown: 2, speed: m.speed || 80 };
let sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx || 0, vy: m.vy || 0, grounded: false, inWater: false, aiT: 0, dir: m.dir || 1, dead: false, fuse: m.fuse || 0, shootCooldown: 2, speed: m.speed || 80 };
applyMobSize(sm);
state.serverMobs.set(m.id, sm);
}
}
// Set myTgId from server if provided
if (data.tg_id) state.myTgId = data.tg_id;
});
// Игрок присоединился
socket.on('player_joined', (data) => {
console.log('Player joined:', data.socket_id);
if (data.socket_id !== state.mySocketId) {
// Генерируем безопасную позицию для нового игрока
const spawnGX = 6;
genColumn(spawnGX);
const surfaceY = surfaceGyAt(spawnGX);
@ -172,37 +185,32 @@ export function initSocket() {
name: data.player_name || 'Игрок'
});
addChatMessage('Система', `Игрок присоединился`);
// Обновляем видимость кнопки сохранения
updateSaveButtonVisibility();
}
});
// Игрок переместился
socket.on('player_moved', (data) => {
if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) {
const p = state.otherPlayers.get(data.socket_id);
p.x = data.x;
p.y = data.y;
// Обновляем имя, если оно пришло
if (data.player_name) {
p.name = data.player_name;
}
}
});
// Игрок покинул
socket.on('player_left', (data) => {
console.log('Player left:', data.socket_id);
state.otherPlayers.delete(data.socket_id);
addChatMessage('Система', `Игрок покинул игру`);
// Обновляем видимость кнопки сохранения
updateSaveButtonVisibility();
});
// === MOB SYNC (multiplayer) ===
// === MOB SYNC ===
socket.on('mob_spawned', (data) => {
const sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 };
let sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 };
applyMobSize(sm);
state.serverMobs.set(data.id, sm);
});
@ -218,7 +226,6 @@ export function initSocket() {
socket.on('mob_died', (data) => {
const sm = state.serverMobs.get(data.id);
if (sm && data.killer === state.mySocketId) {
// Give loot to the killer
if (sm.kind === 'chicken') playSound('hurt_chicken');
state.inv.meat += (sm.kind === 'chicken' ? 1 : 2);
if (sm.kind === 'skeleton') {
@ -247,7 +254,6 @@ export function initSocket() {
});
});
// Блок изменён
socket.on('block_changed', (data) => {
const key = k(data.gx, data.gy);
state.serverOverrides.set(key, { op: data.op, t: data.t });
@ -258,13 +264,11 @@ export function initSocket() {
}
});
// Сообщение в чат
socket.on('chat_message', (data) => {
const senderName = data.socket_id === state.mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`;
addChatMessage(senderName, data.message);
});
// Обновление времени
socket.on('time_update', (data) => {
if (data.time !== undefined) {
state.worldTime = data.time;
@ -276,4 +280,4 @@ export function initSocket() {
console.error('Error initializing socket:', e);
state.isMultiplayer = false;
}
}
}

View File

@ -1,19 +1,20 @@
// Голосовой чат
import { state } from '../core/state.js';
import { worldId } from '../config.js';
import { VOICE_SERVER } from '../core/constants.js';
let voiceSocket = null;
let voiceStream = null;
let audioCtx = null;
let voiceProcessor = null;
let voiceActive = false;
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
// Кнопка микрофона
const voiceBtn = document.createElement('div');
voiceBtn.innerHTML = '🎤<span style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:36px;color:#e74c3c;font-weight:bold;">/</span>';
voiceBtn.title = 'Голосовой чат (выкл)';
voiceBtn.style.cssText = 'position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;';
voiceBtn.style.cssText = 'position:absolute;top:10px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;';
document.querySelector('.ui').appendChild(voiceBtn);
// Индикатор говорящего
@ -64,19 +65,20 @@ voiceBtn.onclick = async () => {
// Подключаемся к голосовому серверу
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
voiceSocket.on('connect', () => {
voiceSocket.emit('voice_join', { world_id: worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' });
voiceSocket.emit('voice_join', { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' });
});
voiceSocket.on('voice_in', (payload) => {
// Воспроизводим входящий голос
const { data, meta, volume } = payload;
const { data, meta, volume: rawVolume } = payload;
const volume = rawVolume !== undefined ? rawVolume : 1.0;
if (!audioCtx || audioCtx.state === 'closed') return;
// Int16 → Float32
// Int16 → Float32 (volume applied via gain node, not in conversion)
const int16 = new Int16Array(data);
const float32 = new Float32Array(int16.length);
for (let i = 0; i < int16.length; i++) {
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF) * volume;
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF);
}
const buf = audioCtx.createBuffer(1, float32.length, 24000);

View File

@ -7,7 +7,14 @@ function isWaterAt(px, py) {
const gx = Math.floor(px / TILE);
const gy = Math.floor(py / TILE);
const b = getBlock(gx, gy);
return !!(b && b.t === 'water');
return !!(b && (b.t === 'water' || b.t === 'swamp_water'));
}
function isSwampWaterAt(px, py) {
const gx = Math.floor(px / TILE);
const gy = Math.floor(py / TILE);
const b = getBlock(gx, gy);
return !!(b && b.t === 'swamp_water');
}
function updateWaterFlag(e) {
@ -19,6 +26,9 @@ function updateWaterFlag(e) {
const feet = isWaterAt(cx, e.y + e.h - 2);
e.inWater = mid || feet;
// Болотная вода
e.inSwampWater = isSwampWaterAt(cx, e.y + e.h / 2);
// Голова под водой — для кислорода/урона
e.headInWater = isWaterAt(cx, e.y + 4);
@ -26,6 +36,11 @@ function updateWaterFlag(e) {
if (e === state.player && !wasInWater && e.inWater && e.vy > 100) {
playSound('splash');
}
// Болотная вода — отравление
if (e === state.player && e.inSwampWater) {
state.player.hp -= 3 * (typeof dt !== 'undefined' ? dt : 0.016);
}
}
export { isWaterAt, updateWaterFlag };
export { isWaterAt, isSwampWaterAt, updateWaterFlag };

View File

@ -17,10 +17,10 @@ export function renderLighting(ctx, W, H, camX, camY, lightC, lightCtx, dpr, blo
// Функция: рисуем мягкий луч света с затуханием за стенами
function castLight(sx, sy, radius) {
const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06;
const flick = 0.97 + Math.sin(now / 300 + sx * 0.01) * 0.015 + Math.sin(now / 470 + sy * 0.02) * 0.015;
const r = radius * flick;
// 12 лучей — достаточно для мягкого круга
const steps = 12;
const steps = 24;
// Собираем дистанции до стен по лучам
const dists = new Float32Array(steps);
for (let i = 0; i < steps; i++) {
@ -29,7 +29,7 @@ export function renderLighting(ctx, W, H, camX, camY, lightC, lightCtx, dpr, blo
const dy = Math.sin(angle);
let maxDist = r;
// Идём по лучу пока не упрёмся в стену
for (let step = TILE * 0.5; step < r; step += TILE * 0.6) {
for (let step = TILE * 0.3; step < r; step += TILE * 0.35) {
const gx = Math.floor((sx + dx * step) / TILE);
const gy = Math.floor((sy + dy * step) / TILE);
const blk = getBlock(gx, gy);
@ -87,13 +87,13 @@ export function renderLighting(ctx, W, H, camX, camY, lightC, lightCtx, dpr, blo
if (b.gx < minGX - 3 || b.gx > maxGX + 3 || b.gy < minGY - 3 || b.gy > maxGY + 3) continue;
const def = BLOCKS[b.t];
if (def.lightRadius) {
const flick = 0.7 + Math.sin(now / 90 + b.gx * 3.7) * 0.15 + Math.sin(now / 140 + b.gy * 2.3) * 0.15;
const flick = 0.9 + Math.sin(now / 350 + b.gx * 3.7) * 0.05 + Math.sin(now / 530 + b.gy * 2.3) * 0.05;
const wx = b.gx * TILE + TILE / 2 - camX;
const wy = b.gy * TILE + TILE / 2 - camY;
const r = def.lightRadius * 0.6 * flick;
const r = def.lightRadius * 0.85;
const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r);
grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`);
grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`);
grad.addColorStop(0, `rgba(255,180,80,0.28)`);
grad.addColorStop(0.5, `rgba(255,140,40,0.13)`);
grad.addColorStop(1, 'rgba(255,100,20,0)');
ctx.fillStyle = grad;
ctx.beginPath();

View File

@ -3,6 +3,313 @@ import { BLOCKS } from '../data/blocks.js';
export const tex = {};
// --- Item/tool sprite generator ---\n
export const itemTex = {};
function drawArrow(ctx, color) {
ctx.fillStyle = '#8B4513';
ctx.fillRect(8, 14, 16, 3);
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(24, 15);
ctx.lineTo(18, 10);
ctx.lineTo(18, 20);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#e74c3c';
ctx.beginPath();
ctx.moveTo(8, 15);
ctx.lineTo(4, 10);
ctx.lineTo(10, 14);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(8, 16);
ctx.lineTo(4, 21);
ctx.lineTo(10, 17);
ctx.closePath();
ctx.fill();
}
function drawPickaxe(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || '#8B4513';
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-8, -3, 16, 5);
ctx.fillRect(-8, -3, 3, 8);
ctx.fillRect(5, -3, 3, 8);
ctx.restore();
}
function drawAxe(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || '#8B4513';
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-2, -8, 10, 8);
ctx.fillRect(-2, -8, 4, 12);
ctx.restore();
}
function drawShovel(ctx, headColor, handleColor) {
ctx.fillStyle = handleColor || '#8B4513';
ctx.save();
ctx.translate(16, 16);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-2, 0, 4, 18);
ctx.fillStyle = headColor;
ctx.fillRect(-6, -8, 12, 8);
ctx.fillRect(-4, -12, 8, 4);
ctx.restore();
}
function drawSword(ctx, bladeColor, guardColor) {
ctx.fillStyle = '#8B4513';
ctx.fillRect(14, 18, 4, 10);
ctx.fillStyle = guardColor || '#f1c40f';
ctx.fillRect(10, 16, 12, 3);
ctx.fillStyle = bladeColor;
ctx.beginPath();
ctx.moveTo(16, 2);
ctx.lineTo(12, 16);
ctx.lineTo(20, 16);
ctx.closePath();
ctx.fill();
}
function drawBow(ctx) {
ctx.strokeStyle = '#8B4513';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7);
ctx.stroke();
ctx.strokeStyle = '#ddd';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(12, 4);
ctx.lineTo(12, 28);
ctx.stroke();
}
function drawIngot(ctx, color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.moveTo(6, 12);
ctx.lineTo(26, 12);
ctx.lineTo(24, 24);
ctx.lineTo(8, 24);
ctx.closePath();
ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(8, 8, 16, 4);
}
function drawMeat(ctx, cooked) {
ctx.fillStyle = cooked ? '#8B4513' : '#e74c3c';
ctx.beginPath();
ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f5f5dc';
ctx.fillRect(8, 6, 3, 12);
ctx.fillRect(14, 4, 3, 8);
}
function makeItemTex(id) {
const c = document.createElement('canvas');
c.width = 32; c.height = 32;
const g = c.getContext('2d');
const tierColors = {
wood: { head: '#a0522d', blade: '#a0522d' },
stone: { head: '#7f8c8d', blade: '#95a5a6' },
iron: { head: '#bdc3c7', blade: '#ecf0f1' },
};
switch (id) {
case 'arrow':
drawArrow(g, '#7f8c8d');
break;
case 'bow':
drawBow(g);
break;
case 'wood_pickaxe':
drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade);
break;
case 'stone_pickaxe':
drawPickaxe(g, tierColors.stone.head);
break;
case 'iron_pickaxe':
drawPickaxe(g, tierColors.iron.head);
break;
case 'wood_axe':
drawAxe(g, tierColors.wood.head);
break;
case 'stone_axe':
drawAxe(g, tierColors.stone.head);
break;
case 'iron_axe':
drawAxe(g, tierColors.iron.head);
break;
case 'wood_shovel':
drawShovel(g, tierColors.wood.head);
break;
case 'stone_shovel':
drawShovel(g, tierColors.stone.head);
break;
case 'iron_shovel':
drawShovel(g, tierColors.iron.head);
break;
case 'wood_sword':
drawSword(g, tierColors.wood.blade, '#a0522d');
break;
case 'stone_sword':
drawSword(g, tierColors.stone.blade, '#7f8c8d');
break;
case 'iron_sword':
drawSword(g, tierColors.iron.blade, '#bdc3c7');
break;
case 'iron_ingot':
drawIngot(g, '#bdc3c7');
break;
case 'gold_ingot':
drawIngot(g, '#f1c40f');
break;
case 'copper_ingot':
drawIngot(g, '#e67e22');
break;
case 'meat':
drawMeat(g, false);
break;
case 'cooked':
drawMeat(g, true);
break;
case 'wheat':
g.fillStyle = '#f1c40f';
g.fillRect(12, 8, 3, 18);
g.fillRect(8, 6, 10, 4);
break;
case 'bread':
g.fillStyle = '#d4a030';
g.fillRect(6, 12, 20, 12);
g.fillStyle = '#c08820';
g.fillRect(6, 12, 20, 3);
break;
case 'carrot':
g.fillStyle = '#e67e22';
g.fillRect(14, 10, 4, 18);
g.fillStyle = '#27ae60';
g.fillRect(12, 6, 8, 5);
break;
case 'potato':
g.fillStyle = '#d4a050';
g.beginPath();
g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#b88a40';
g.fillRect(10, 14, 2, 1);
g.fillRect(20, 16, 2, 1);
break;
case 'baked_potato':
g.fillStyle = '#c08820';
g.beginPath();
g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#f5e6cc';
g.fillRect(12, 12, 8, 6);
break;
case 'mushroom_stew':
g.fillStyle = '#8B4513';
g.fillRect(10, 16, 12, 12);
g.fillStyle = '#e74c3c';
g.beginPath();
g.ellipse(16, 14, 8, 5, 0, Math.PI, 0);
g.fill();
break;
case 'scorpion_stinger':
g.fillStyle = '#d35400';
g.beginPath();
g.moveTo(16, 4);
g.lineTo(10, 28);
g.lineTo(22, 28);
g.closePath();
g.fill();
break;
case 'polar_fur':
g.fillStyle = '#ecf0f1';
g.fillRect(6, 6, 20, 20);
g.fillStyle = '#bdc3c7';
g.fillRect(12, 12, 8, 8);
break;
case 'slime_ball':
g.fillStyle = 'rgba(46,204,113,0.8)';
g.beginPath();
g.arc(16, 16, 10, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#fff';
g.fillRect(10, 10, 4, 4);
g.fillRect(18, 10, 4, 4);
break;
case 'eagle_feather':
g.fillStyle = '#8B4513';
g.fillRect(14, 4, 4, 24);
g.fillStyle = '#ecf0f1';
g.fillRect(10, 6, 12, 10);
break;
case 'chicken_meat':
drawMeat(g, false);
g.fillStyle = '#fff';
g.fillRect(12, 8, 4, 4);
break;
case 'feather':
g.fillStyle = '#ecf0f1';
g.fillRect(14, 6, 4, 20);
g.fillStyle = '#bbb';
g.fillRect(12, 8, 8, 8);
break;
case 'bone':
g.fillStyle = '#f5f5dc';
g.fillRect(12, 10, 8, 14);
g.fillRect(8, 8, 4, 4);
g.fillRect(20, 8, 4, 4);
g.fillRect(8, 22, 4, 4);
g.fillRect(20, 22, 4, 4);
break;
case 'gunpowder':
g.fillStyle = '#555';
g.fillRect(8, 10, 6, 6);
g.fillRect(18, 14, 6, 6);
g.fillRect(12, 20, 6, 6);
break;
case 'gold_armor':
g.fillStyle = '#f1c40f';
g.fillRect(6, 6, 20, 20);
g.fillStyle = '#f39c12';
g.fillRect(10, 4, 12, 6);
g.fillRect(4, 10, 6, 16);
g.fillRect(22, 10, 6, 16);
break;
case 'wheat_seeds':
g.fillStyle = '#7dcea0';
g.fillRect(14, 16, 4, 10);
g.fillStyle = '#a8e6a0';
g.fillRect(10, 8, 4, 4);
g.fillRect(18, 6, 4, 4);
g.fillRect(14, 4, 4, 4);
break;
default:
g.fillStyle = '#7f8c8d';
g.fillRect(4, 4, 24, 24);
g.fillStyle = 'rgba(255,255,255,0.2)';
g.fillRect(4, 4, 24, 4);
}
return c;
}
export function makeTex(type) {
const t = BLOCKS[type];
const c = document.createElement('canvas');
@ -38,16 +345,12 @@ export function makeTex(type) {
return c;
}
if (type === 'bed') {
// Основание кровати
g.fillStyle = '#e91e63';
g.fillRect(0, 0, 32, 32);
// Подушка
g.fillStyle = '#f8bbd0';
g.fillRect(2, 2, 14, 14);
// Одеяло
g.fillStyle = '#c2185b';
g.fillRect(16, 4, 14, 24);
// Детали одеяла
g.fillStyle = '#e91e63';
g.fillRect(18, 6, 10, 20);
return c;
@ -58,26 +361,20 @@ export function makeTex(type) {
return c;
}
if (type === 'boat') {
// Корпус лодки
g.fillStyle = '#8B4513';
g.fillRect(2, 12, 28, 8);
// Борта
g.fillStyle = '#A0522D';
g.fillRect(0, 10, 32, 12);
// Внутренность
g.fillStyle = '#DEB887';
g.fillRect(4, 14, 24, 4);
// Дно
g.fillStyle = '#654321';
g.fillRect(2, 20, 28, 4);
return c;
}
if (type === 'ladder') {
// Боковые стойки лестницы
g.fillStyle = '#8B4513';
g.fillRect(4, 0, 4, 32);
g.fillRect(24, 0, 4, 32);
// Ступени
g.fillStyle = '#A0522D';
g.fillRect(4, 4, 24, 3);
g.fillRect(4, 12, 24, 3);
@ -85,6 +382,96 @@ export function makeTex(type) {
g.fillRect(4, 28, 24, 3);
return c;
}
// === BIOME BLOCK TEXTURES ===
if (type === 'snow') {
g.fillStyle = '#ecf0f1';
g.fillRect(0, 0, 32, 32);
g.fillStyle = '#dfe6e9';
for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3);
return c;
}
if (type === 'ice') {
g.fillStyle = '#74b9ff';
g.fillRect(0, 0, 32, 32);
g.strokeStyle = 'rgba(255,255,255,0.4)';
g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke();
g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke();
return c;
}
if (type === 'cactus') {
g.fillStyle = '#27ae60';
g.fillRect(6, 2, 20, 28);
g.fillStyle = '#2ecc71';
g.fillRect(2, 8, 6, 4);
g.fillRect(24, 14, 6, 4);
g.fillStyle = '#1e8449';
g.fillRect(12, 0, 2, 30);
g.fillRect(18, 0, 2, 30);
return c;
}
if (type === 'mushroom') {
g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12);
g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill();
g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3);
return c;
}
if (type === 'moss') {
g.fillStyle = '#0a6640';
g.fillRect(0, 0, 32, 32);
g.fillStyle = '#1e8449';
for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3);
return c;
}
if (type === 'swamp_water') {
g.fillStyle = 'rgba(100,120,40,0.6)';
g.fillRect(0, 0, 32, 32);
g.fillStyle = 'rgba(80,100,20,0.3)';
g.fillRect(0, 10, 32, 2);
g.fillRect(0, 22, 32, 2);
return c;
}
if (type === 'farmland') {
g.fillStyle = '#8B6914';
g.fillRect(0, 0, 32, 32);
g.fillStyle = '#7a5c10';
for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2);
g.fillStyle = '#6B4E0A';
g.fillRect(8, 2, 2, 28);
g.fillRect(18, 2, 2, 28);
return c;
}
if (type === 'dead_bush') {
g.fillStyle = '#b2bec3';
g.fillRect(12, 16, 2, 16);
g.fillRect(8, 14, 8, 2);
g.fillRect(16, 12, 8, 2);
g.fillRect(6, 18, 4, 2);
return c;
}
if (type === 'spruce_leaves') {
g.fillStyle = '#0a6640';
g.fillRect(0, 0, 32, 32);
g.fillStyle = '#0d7a4d';
for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4);
return c;
}
// CROP STAGES
if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) {
const st = parseInt(type.charAt(type.length-1));
const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] :
type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] :
['#a8e6a0','#c8b888','#bfaa78','#dfe6e9'];
g.fillStyle = '#5d4037';
g.fillRect(15, 18, 2, 14);
if (st >= 1) {
g.fillStyle = colors[st];
g.fillRect(10, 8 + (3-st)*3, 12, 10);
} else {
g.fillStyle = '#a8e6a0';
g.fillRect(14, 22, 4, 4);
}
return c;
}
g.fillStyle = t.c || '#000';
g.fillRect(0, 0, 32, 32);
@ -101,4 +488,19 @@ export function makeTex(type) {
export function initTextures() {
Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k));
}
// Item/tool sprites
const itemIds = [
'arrow','bow',
'wood_pickaxe','stone_pickaxe','iron_pickaxe',
'wood_axe','stone_axe','iron_axe',
'wood_shovel','stone_shovel','iron_shovel',
'wood_sword','stone_sword','iron_sword',
'iron_ingot','gold_ingot','copper_ingot',
'meat','cooked',
'wheat','bread','carrot','potato','baked_potato','mushroom_stew','wheat_seeds',
'chicken_meat','feather','bone','gunpowder',
'scorpion_stinger','polar_fur','slime_ball','eagle_feather',
'gold_armor',
];
itemIds.forEach(id => { itemTex[id] = makeItemTex(id); });
}

View File

@ -1,9 +1,9 @@
// ==================== ЧАТ ====================
import { state } from '../core/state.js';
import { MAX_CHAT_MESSAGES } from '../core/constants.js';
import { playSound } from '../audio/sound-engine.js';
const chatMessages = [];
const MAX_CHAT_MESSAGES = 20;
export function addChatMessage(sender, message) {
const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });

View File

@ -5,19 +5,15 @@ import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { RECIPES } from '../data/recipes.js';
import { addTool } from '../data/tools.js';
import { tex } from '../render/textures.js';
import { tex, itemTex } from '../render/textures.js';
import { playSound } from '../audio/sound-engine.js';
import { rebuildHotbar } from './hotbar.js';
import { renderInventory } from './inventory.js';
export function canCraft(r) {
const inv = state.inv;
console.log('[CRAFT] Checking recipe for:', r.out, '- cost:', r.cost);
for (const res in r.cost) {
const have = inv[res] || 0;
const need = r.cost[res];
console.log('[CRAFT] Resource:', res, '- have:', have, '- need:', need, '- can craft:', have >= need);
if (have < need) return false;
if ((inv[res] || 0) < r.cost[res]) return false;
}
return true;
}
@ -31,17 +27,19 @@ export function renderCraft() {
row.className = 'recipe';
const icon = document.createElement('div');
icon.className = 'ricon';
// Иконка — блок, инструмент или предмет
// Иконка результата — canvas sprite > emoji
if (tex[r.out]) {
icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`;
} else if (itemTex[r.out]) {
icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`;
} else if (TOOLS[r.out]) {
icon.textContent = TOOLS[r.out].icon;
icon.textContent = TOOLS[r.out].icon || '🔧';
icon.style.fontSize = '24px';
icon.style.display = 'flex';
icon.style.alignItems = 'center';
icon.style.justifyContent = 'center';
} else if (ITEMS[r.out]) {
icon.textContent = ITEMS[r.out].icon;
icon.textContent = ITEMS[r.out].icon || '📦';
icon.style.fontSize = '24px';
icon.style.display = 'flex';
icon.style.alignItems = 'center';
@ -70,8 +68,10 @@ export function renderCraft() {
for (const res in r.cost) inv[res] -= r.cost[res];
inv[r.out] = (inv[r.out] || 0) + r.qty;
if (TOOLS[r.out]) addTool(r.out);
if (!state.recentItems.includes(r.out)) { state.recentItems.unshift(r.out); state.recentItems = state.recentItems.slice(0, 5); }
rebuildHotbar();
renderCraft();
renderInventory();
};
row.appendChild(icon); row.appendChild(info); row.appendChild(btn);
recipesEl.appendChild(row);
@ -82,35 +82,33 @@ let craftOpen = false;
let inventoryOpen = false;
export function toggleCraft() {
playSound('click'); // Звук клика по кнопке
playSound('click');
craftOpen = !craftOpen;
state.craftPanel.style.display = craftOpen ? 'block' : 'none';
if (craftOpen) {
renderCraft();
// Закрываем инвентарь если открыт крафт
inventoryOpen = false;
state.inventoryPanel.style.display = 'none';
}
}
export function closeCraft() {
playSound('click'); // Звук клика по кнопке
playSound('click');
craftOpen = false;
state.craftPanel.style.display = 'none';
}
export function toggleInventory() {
playSound('click'); // Звук клика по кнопке
playSound('click');
inventoryOpen = true;
state.inventoryPanel.style.display = 'block';
renderInventory();
// Закрываем крафт если открыт инвентарь
craftOpen = false;
state.craftPanel.style.display = 'none';
}
export function closeInventory() {
playSound('click'); // Звук клика по кнопке
playSound('click');
inventoryOpen = false;
state.inventoryPanel.style.display = 'none';
}
}

View File

@ -11,4 +11,8 @@ export const craftPanel = document.getElementById('craftPanel');
export const recipesEl = document.getElementById('recipes');
export const deathEl = document.getElementById('death');
export const inventoryPanel = document.getElementById('inventoryPanel');
export const inventoryGrid = document.getElementById('inventoryGrid');
export const inventoryGrid = document.getElementById('inventoryGrid');
export const marketPanel = document.getElementById('marketPanel');
export const marketContent = document.getElementById('marketContent');
export const tradePanel = document.getElementById('tradePanel');
export const tradeContent = document.getElementById('tradeContent');

View File

@ -1,81 +1,151 @@
// Hotbar UI
// Hotbar UI — 3-slot Minecraft-style bottom bar + selected item indicator
import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { tex } from '../render/textures.js';
import { tex, itemTex } from '../render/textures.js';
import { playSound } from '../audio/sound-engine.js';
const HOTBAR_SLOTS = 3;
let selectedItemEl = null;
export function addToRecentItems(id) {
if (!id) return;
state.recentItems = state.recentItems.filter(item => item !== id);
state.recentItems.unshift(id);
state.recentItems = state.recentItems.slice(0, 5);
}
function renderSlotIcon(el, id) {
if (BLOCKS[id] && tex[id]) {
el.style.backgroundImage = `url(${tex[id].toDataURL()})`;
el.style.backgroundSize = 'cover';
} else if (itemTex[id]) {
el.style.backgroundImage = `url(${itemTex[id].toDataURL()})`;
el.style.backgroundSize = 'cover';
} else if (ITEMS[id]) {
el.textContent = ITEMS[id].icon || '📦';
} else if (TOOLS[id]) {
el.textContent = TOOLS[id].icon || '🔧';
} else if (id === 'iron_armor') {
el.textContent = '🛡️';
el.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)';
} else {
el.textContent = id.charAt(0).toUpperCase();
el.style.background = '#555';
el.style.color = '#fff';
el.style.fontWeight = '700';
el.style.fontSize = '14px';
}
}
function updateSelectedItem() {
if (!selectedItemEl) selectedItemEl = document.getElementById('selectedItem');
if (!selectedItemEl) return;
const sel = state.selected;
if (!sel || !state.inv[sel] || state.inv[sel] <= 0) {
selectedItemEl.style.display = 'none';
return;
}
selectedItemEl.style.display = 'flex';
// Clear previous content
selectedItemEl.textContent = '';
selectedItemEl.style.backgroundImage = '';
selectedItemEl.style.background = 'rgba(0,0,0,0.45)';
// Render icon
if (BLOCKS[sel] && tex[sel]) {
selectedItemEl.style.backgroundImage = `url(${tex[sel].toDataURL()})`;
selectedItemEl.style.backgroundSize = 'cover';
} else if (itemTex[sel]) {
selectedItemEl.style.backgroundImage = `url(${itemTex[sel].toDataURL()})`;
selectedItemEl.style.backgroundSize = 'cover';
} else if (ITEMS[sel]) {
selectedItemEl.textContent = ITEMS[sel].icon || '📦';
} else if (TOOLS[sel]) {
selectedItemEl.textContent = TOOLS[sel].icon || '🔧';
} else {
selectedItemEl.textContent = sel.charAt(0).toUpperCase();
}
}
export function rebuildHotbar() {
const hotbarEl = state.hotbarEl;
const inv = state.inv;
const selected = state.selected;
const recentItems = state.recentItems;
const toolDurability = state.toolDurability;
hotbarEl.innerHTML = '';
// Показываем последние 5 выбранных предметов (если они есть в инвентаре)
const items = recentItems.filter(id => inv[id] > 0).slice(0, 5);
// Get up to 3 recent items with count > 0
const items = state.recentItems.filter(id => inv[id] > 0).slice(0, HOTBAR_SLOTS);
for (const id of items) {
// If selected item not in list and has count > 0, push to front
if (inv[selected] > 0 && !items.includes(selected)) {
items.unshift(selected);
if (items.length > HOTBAR_SLOTS) items.pop();
}
// Always render HOTBAR_SLOTS slots (fill empties)
for (let si = 0; si < HOTBAR_SLOTS; si++) {
const id = items[si] || null;
const s = document.createElement('div');
s.className = 'slot' + (id === selected ? ' sel' : '');
if (BLOCKS[id]) {
s.style.backgroundImage = `url(${tex[id].toDataURL()})`;
s.style.backgroundSize = 'cover';
} else if (ITEMS[id]) {
s.textContent = ITEMS[id].icon;
} else if (TOOLS[id]) {
s.textContent = TOOLS[id].icon;
} else if (id === 'iron_armor') {
s.textContent = '🛡️';
s.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)';
}
const c = document.createElement('div');
c.className = 'count';
c.textContent = inv[id];
s.appendChild(c);
s.onclick = () => {
playSound('click'); // Звук клика по инвентарю
state.selected = id;
// Обновляем список последних предметов
state.recentItems = state.recentItems.filter(item => item !== id); // Удаляем если уже есть
state.recentItems.unshift(id); // Добавляем в начало
state.recentItems = state.recentItems.slice(0, 5); // Оставляем только 5
rebuildHotbar();
};
s.className = 'slot' + (id && id === selected ? ' sel' : '');
// Показываем индикатор надетой брони
if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') {
const equipped = document.createElement('div');
equipped.className = 'equipped-indicator';
equipped.textContent = '✓';
s.appendChild(equipped);
}
// Slot number indicator (top-left)
const numEl = document.createElement('div');
numEl.style.cssText = 'position:absolute;top:1px;left:3px;font-size:10px;color:rgba(255,255,255,0.55);font-weight:700;pointer-events:none;text-shadow:1px 1px 1px rgba(0,0,0,0.7);';
numEl.textContent = (si + 1).toString();
s.appendChild(numEl);
// Durability bar для инструментов
if (TOOLS[id] && inv[id] > 0) {
// Находим текущую прочность
let curDur = 0, maxDur = TOOLS[id].durability;
for (const [tid, dur] of toolDurability) {
if (dur.type === id) {
curDur = dur.current;
maxDur = dur.max;
break;
}
if (id) {
renderSlotIcon(s, id);
const c = document.createElement('div');
c.className = 'count';
c.textContent = inv[id];
s.appendChild(c);
s.onclick = () => {
playSound('click');
state.selected = id;
// Move to front of recentItems
state.recentItems = state.recentItems.filter(item => item !== id);
state.recentItems.unshift(id);
rebuildHotbar();
};
if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') {
const equipped = document.createElement('div');
equipped.className = 'equipped-indicator';
equipped.textContent = '✓';
s.appendChild(equipped);
}
if (maxDur > 0) {
const bar = document.createElement('div');
bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`;
const fill = document.createElement('div');
const pct = curDur / maxDur;
const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c';
fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`;
bar.appendChild(fill);
s.appendChild(bar);
// Durability bar
if (TOOLS[id] && inv[id] > 0) {
let curDur = 0, maxDur = TOOLS[id].durability;
for (const [tid, dur] of toolDurability) {
if (dur.type === id) {
curDur = dur.current;
maxDur = dur.max;
break;
}
}
if (maxDur > 0) {
const bar = document.createElement('div');
bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`;
const fill = document.createElement('div');
const pct = curDur / maxDur;
const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c';
fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`;
bar.appendChild(fill);
s.appendChild(bar);
}
}
}
hotbarEl.appendChild(s);
}
// Update selected item indicator above hotbar
updateSelectedItem();
}

View File

@ -3,7 +3,7 @@ import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { tex } from '../render/textures.js';
import { tex, itemTex } from '../render/textures.js';
import { playSound } from '../audio/sound-engine.js';
import { rebuildHotbar } from './hotbar.js';
@ -13,26 +13,35 @@ export function renderInventory() {
const selected = state.selected;
inventoryGrid.innerHTML = '';
// Создаём сетку инвентаря 7x3
const items = Object.keys(inv).filter(id => inv[id] > 0);
// Добавляем пустые слоты для полной сетки
for (let i = 0; i < 21; i++) {
const slot = document.createElement('div');
slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : '');
if (i < items.length) {
const id = items[i];
if (BLOCKS[id]) {
// Canvas sprite: block > itemTex > emoji fallback > unknown item fallback
if (BLOCKS[id] && tex[id]) {
slot.style.backgroundImage = `url(${tex[id].toDataURL()})`;
slot.style.backgroundSize = 'cover';
} else if (itemTex[id]) {
slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`;
slot.style.backgroundSize = 'cover';
} else if (ITEMS[id]) {
slot.textContent = ITEMS[id].icon;
slot.textContent = ITEMS[id].icon || '📦';
} else if (TOOLS[id]) {
slot.textContent = TOOLS[id].icon;
slot.textContent = TOOLS[id].icon || '🔧';
} else if (id === 'iron_armor') {
slot.textContent = '🛡️';
slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)';
} else {
// Unknown item fallback — grey box with first letter
slot.textContent = id.charAt(0).toUpperCase();
slot.style.background = '#555';
slot.style.color = '#fff';
slot.style.fontWeight = '700';
slot.style.fontSize = '14px';
}
const count = document.createElement('div');
@ -41,12 +50,11 @@ export function renderInventory() {
slot.appendChild(count);
slot.onclick = () => {
playSound('click'); // Звук клика по инвентарю
playSound('click');
state.selected = id;
// Обновляем список последних предметов
state.recentItems = state.recentItems.filter(item => item !== id); // Удаляем если уже есть
state.recentItems.unshift(id); // Добавляем в начало
state.recentItems = state.recentItems.slice(0, 5); // Оставляем только 5
state.recentItems = state.recentItems.filter(item => item !== id);
state.recentItems.unshift(id);
state.recentItems = state.recentItems.slice(0, 5);
rebuildHotbar();
renderInventory();
};
@ -54,16 +62,12 @@ export function renderInventory() {
// Двойной клик для надевания брони
slot.ondblclick = () => {
if (id === 'iron_armor' && inv.iron_armor > 0) {
// Если уже надета броня - снимаем её
if (state.player.equippedArmor === 'iron_armor') {
state.player.equippedArmor = null;
state.player.armor = 0;
console.log('[ARMOR] Iron armor unequipped');
} else {
// Надеваем броню
state.player.equippedArmor = 'iron_armor';
state.player.armor = BLOCKS['iron_armor'].armor;
console.log('[ARMOR] Iron armor equipped - armor:', state.player.armor);
}
playSound('click');
renderInventory();
@ -73,4 +77,4 @@ export function renderInventory() {
inventoryGrid.appendChild(slot);
}
}
}

84
src/ui/market.js Normal file
View File

@ -0,0 +1,84 @@
// ==================== РЫНОК ПРЕДЛОЖЕНИЙ ====================
import { state } from '../core/state.js';
import { playSound } from '../audio/sound-engine.js';
let marketPanel, marketClose, marketContent;
export function initMarket() {
marketPanel = document.getElementById('marketPanel');
marketClose = document.getElementById('marketClose');
marketContent = document.getElementById('marketContent');
if (!marketPanel) return;
if (marketClose) marketClose.addEventListener('click', () => closeMarket());
document.addEventListener('toggleMarket', () => toggleMarket());
}
export function toggleMarket() {
if (!marketPanel) return;
if (marketPanel.style.display === 'none' || !marketPanel.style.display) {
openMarket();
} else {
closeMarket();
}
}
export function closeMarket() {
if (marketPanel) marketPanel.style.display = 'none';
}
function openMarket() {
if (marketPanel) {
marketPanel.style.display = 'block';
renderMarket();
}
}
function renderMarket() {
if (!marketContent) return;
if (!state.socket) {
marketContent.innerHTML = '<p style="color:#aaa;">Подключение к серверу...</p>';
return;
}
state.socket.emit('market_list', {}, (orders) => {
if (!orders || !orders.length) {
marketContent.innerHTML = '<p style="color:#aaa;text-align:center;">Предложений пока нет</p><div style="margin-top:12px;"><button id="createOrderBtn" style="width:100%;padding:10px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">+ Выставить предмет</button></div>';
const btn = document.getElementById('createOrderBtn');
if (btn) btn.addEventListener('click', showCreateOrder);
return;
}
let html = '<div style="margin-bottom:8px;"><button id="createOrderBtn" style="padding:8px 14px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">+ Выставить предмет</button></div>';
html += orders.map(o => '<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.1);"><div><span style="color:#ffd700;">' + (o.offer_item||'') + '</span> x' + (o.offer_qty||1) + ' <span style="color:#888;"> ⇄ </span><span style="color:#7ecfff;">' + (o.want_item||'') + '</span> x' + (o.want_qty||1) + '</div><div style="font-size:11px;color:#888;">' + (o.seller || 'Аноним') + '</div></div>').join('');
marketContent.innerHTML = html;
const btn = document.getElementById('createOrderBtn');
if (btn) btn.addEventListener('click', showCreateOrder);
});
}
function showCreateOrder() {
if (!marketContent) return;
const invKeys = Object.keys(state.inv || {});
const invOpts = invKeys.filter(k => (state.inv[k]||0) > 0).map(k => '<option value="'+k+'">'+k+' (x'+state.inv[k]+')</option>').join('');
marketContent.innerHTML = '<div style="padding:8px;"><h3 style="margin:0 0 10px;font-size:14px;">Выставить предмет</h3><label style="font-size:12px;color:#aaa;">Отдаю:</label><select id="offerItem" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;">'+invOpts+'</select><label style="font-size:12px;color:#aaa;">Хочу:</label><input id="wantItem" placeholder="Название предмета" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;" /><div style="display:flex;gap:8px;"><button id="submitOrder" style="flex:1;padding:8px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;">Выставить</button><button id="cancelOrder" style="flex:1;padding:8px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">Отмена</button></div></div>';
const cancelBtn = document.getElementById('cancelOrder');
if (cancelBtn) cancelBtn.addEventListener('click', () => renderMarket());
const submitBtn = document.getElementById('submitOrder');
if (submitBtn) submitBtn.addEventListener('click', () => {
const offerItem = document.getElementById('offerItem');
const wantItem = document.getElementById('wantItem');
if (!offerItem || !wantItem) return;
const offer = offerItem.value;
const want = wantItem.value.trim();
if (!offer || !want) { alert('Выберите предмет и укажите что хотите получить'); return; }
if (state.socket) {
state.socket.emit('market_create', { offer_item: offer, offer_qty: 1, want_item: want, want_qty: 1 });
playSound('click');
setTimeout(renderMarket, 500);
}
});
}

View File

@ -26,6 +26,15 @@ export function initMinimap() {
state.minimapOpen = !state.minimapOpen;
minimapWrap.style.display = state.minimapOpen ? 'block' : 'none';
};
const minimapClose = document.getElementById('minimapClose');
if (minimapClose) {
minimapClose.addEventListener('click', (e) => {
e.stopPropagation();
state.minimapOpen = false;
minimapWrap.style.display = 'none';
});
}
}
export function renderMinimap() {
@ -110,4 +119,4 @@ export function renderMinimap() {
minimapCtx.fillRect(dx * scale, dy * scale, 2, 2);
}
}
}
}

View File

@ -1,62 +1,156 @@
// ==================== КНОПКИ СОХРАНЕНИЯ / СБРОСА ====================
// ==================== МЕНЮ ⋯ + СОХРАНЕНИЕ / СБРОС ====================
import { state } from '../core/state.js';
import { playSound } from '../audio/sound-engine.js';
import { saveGame } from '../game/save.js';
import { shareWorld } from './share.js';
import { isTgWebApp } from './start-menu.js';
let dropdownOpen = false;
export function initSaveControls() {
const saveBtn = document.getElementById('saveBtn');
saveBtn.onclick = () => {
playSound('click');
saveGame();
alert('Игра сохранена!');
};
const menuBtn = document.getElementById('menuBtn');
const menuDropdown = document.getElementById('menuDropdown');
const saveItem = document.getElementById('saveItem');
const resetItem = document.getElementById('resetItem');
const marketItem = document.getElementById('marketItem');
const shareItem = document.getElementById('shareItem');
const settingsItem = document.getElementById('settingsItem');
const quitItem = document.getElementById('quitItem');
const resetBtn = document.getElementById('resetBtn');
resetBtn.onclick = () => {
if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) {
playSound('click');
// Убить возможный старый onclick
if (menuBtn) menuBtn.onclick = null;
// Удаляем сохранение из localStorage
try {
localStorage.removeItem(state.SAVE_KEY);
console.log('Сохранение удалено из localStorage');
} catch (e) {
console.warn('Ошибка удаления сохранения:', e);
}
// Тогл меню
if (menuBtn) {
menuBtn.addEventListener('click', (e) => {
e.stopPropagation();
dropdownOpen = !dropdownOpen;
if (menuDropdown) menuDropdown.classList.toggle('open', dropdownOpen);
});
}
// Сбрасываем in-memory сохранение
state.inMemorySave = null;
// Генерируем новый worldId
state.worldId = Math.random().toString(36).substring(2, 10);
console.log('Новый worldId после сброса:', state.worldId);
// Обновляем URL
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', state.worldId);
const newUrlString = newUrl.toString();
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
window.history.replaceState(null, '', newUrlString);
console.log('URL обновлён:', newUrlString);
}
} catch (e) {
console.error('Ошибка обновления URL:', e);
}
// Перезагружаем страницу
location.reload();
// Закрытие по клику вне
document.addEventListener('click', () => {
if (dropdownOpen) {
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
}
};
});
if (menuDropdown) {
menuDropdown.addEventListener('click', (e) => e.stopPropagation());
}
// 💾 Сохранить
if (saveItem) {
saveItem.addEventListener('click', () => {
playSound('click');
saveGame();
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
alert('Игра сохранена!');
});
}
// 🔄 Новый мир
if (resetItem) {
resetItem.addEventListener('click', () => {
if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) {
playSound('click');
try { localStorage.removeItem(state.SAVE_KEY); } catch(e) {}
state.inMemorySave = null;
state.worldId = Math.random().toString(36).substring(2, 10);
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', state.worldId);
window.history.replaceState(null, '', newUrl.toString());
} catch(e) {}
location.reload();
} else {
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
}
});
}
// 🏪 Рынок
if (marketItem) {
marketItem.addEventListener('click', () => {
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
document.dispatchEvent(new CustomEvent('toggleMarket'));
});
}
// 📤 Пригласить
if (shareItem) {
shareItem.addEventListener('click', () => {
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
shareWorld();
});
}
// ⚙️ Настройки
if (settingsItem) {
settingsItem.addEventListener('click', () => {
dropdownOpen = false;
menuDropdown.classList.remove('open');
const panel = document.getElementById('settingsPanel');
if (!panel) return;
// Sync controls with current state
const toggle = document.getElementById('dayNightToggle');
const timeSlider = document.getElementById('timeSlider');
const wSlider = document.getElementById('weatherIntensity');
if (toggle) toggle.checked = state.dayNightCycle !== false;
if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100);
if (wSlider) wSlider.value = Math.round(state.rainIntensity * 100);
// Sync weather buttons — highlight the active type
const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear');
const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1);
for (const bid of ['weatherClear','weatherRain','weatherSnow','weatherThunder']) {
const b = document.getElementById(bid);
if (b) b.style.background = (bid === activeId) ? '#3498db' : '#555';
}
panel.style.display = 'block';
});
}
// Закрытие настроек
const settingsClose = document.getElementById('settingsClose');
if (settingsClose) {
settingsClose.addEventListener('click', () => {
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) settingsPanel.style.display = 'none';
});
}
// Тогл день/ночи
const dayNightToggle = document.getElementById('dayNightToggle');
if (dayNightToggle) {
dayNightToggle.addEventListener('change', () => {
state.dayNightCycle = dayNightToggle.checked;
saveGame();
});
}
// 🏠 Главное меню
if (quitItem) {
quitItem.addEventListener('click', () => {
dropdownOpen = false;
if (menuDropdown) menuDropdown.classList.remove('open');
saveGame();
try {
const url = new URL(window.location.href);
url.searchParams.delete('world');
window.location.href = url.toString();
} catch(e) { location.reload(); }
});
}
}
// Показываем кнопку сохранения только если играем одни
export function updateSaveButtonVisibility() {
const saveBtn = document.getElementById('saveBtn');
if (state.isMultiplayer && state.otherPlayers.size > 0) {
saveBtn.style.display = 'none';
} else {
saveBtn.style.display = 'flex';
}
}
const saveItem = document.getElementById('saveItem');
if (!saveItem) return;
saveItem.style.display = (state.isMultiplayer && state.otherPlayers.size > 0) ? 'none' : 'block';
}

127
src/ui/settings.js Normal file
View File

@ -0,0 +1,127 @@
// Настройки
import { state } from '../core/state.js';
let settingsPanel = null;
let dayNightToggle = null;
// Weather button IDs in order
const WEATHER_BTNS = ['weatherClear', 'weatherRain', 'weatherSnow', 'weatherThunder'];
const WEATHER_ACTIVE_BG = '#3498db';
const WEATHER_INACTIVE_BG = '#555';
function setWeatherButtonActive(activeId) {
for (const id of WEATHER_BTNS) {
const btn = document.getElementById(id);
if (btn) {
btn.style.background = (id === activeId) ? WEATHER_ACTIVE_BG : WEATHER_INACTIVE_BG;
}
}
}
export function initSettings() {
settingsPanel = document.getElementById('settingsPanel');
dayNightToggle = document.getElementById('dayNightToggle');
if (!settingsPanel) return;
const closeBtn = document.getElementById('settingsClose');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
settingsPanel.style.display = 'none';
});
}
// Day/night cycle toggle
if (dayNightToggle) {
dayNightToggle.checked = state.dayNightCycle !== false;
dayNightToggle.addEventListener('change', () => {
state.dayNightCycle = dayNightToggle.checked;
});
}
// Time-of-day slider
const timeSlider = document.getElementById('timeSlider');
if (timeSlider) {
timeSlider.value = Math.round(state.worldTime * 100);
timeSlider.addEventListener('input', () => {
state.worldTime = parseInt(timeSlider.value) / 100;
if (state.dayNightCycle) {
state.dayNightCycle = false;
if (dayNightToggle) dayNightToggle.checked = false;
}
});
}
// ====== Weather controls (Ясно / Дождь / Снег / Гроза) ======
const weatherIntensitySlider = document.getElementById('weatherIntensity');
// Helper: apply a weather type
function applyWeather(type) {
state.weatherType = type;
state.weatherChangeInterval = Infinity; // Lock weather manually
switch (type) {
case 'clear':
state.isRaining = false;
state.rainIntensity = 0;
if (weatherIntensitySlider) weatherIntensitySlider.value = 0;
break;
case 'rain':
state.isRaining = true;
state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100;
break;
case 'snow':
state.isRaining = false; // snow uses its own rendering
state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100;
break;
case 'thunder':
state.isRaining = true; // thunder includes rain
state.rainIntensity = parseInt(weatherIntensitySlider?.value || 60) / 100;
if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100);
break;
}
setWeatherButtonActive('weather' + type.charAt(0).toUpperCase() + type.slice(1));
}
// Button handlers
for (const id of WEATHER_BTNS) {
const btn = document.getElementById(id);
if (btn) {
const weatherType = btn.dataset.weather || id.replace('weather', '').toLowerCase();
btn.addEventListener('click', () => applyWeather(weatherType));
}
}
// Intensity slider
if (weatherIntensitySlider) {
weatherIntensitySlider.addEventListener('input', () => {
const val = parseInt(weatherIntensitySlider.value) / 100;
if (state.weatherType !== 'clear') {
state.rainIntensity = val;
if (state.weatherType === 'thunder' || state.weatherType === 'rain') {
state.isRaining = true;
}
}
});
}
// Listen for toggleSettings custom event
document.addEventListener('toggleSettings', () => {
if (settingsPanel) {
// Sync all controls when panel opens
if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100);
if (dayNightToggle) dayNightToggle.checked = state.dayNightCycle !== false;
if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100);
// Highlight active weather button
const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear');
const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1);
setWeatherButtonActive(activeId);
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
}
});
}
export function isDayNightEnabled() {
return state.dayNightCycle !== false;
}

View File

@ -1,38 +1,108 @@
// ==================== ПОДЕЛИТЬСЯ МИРОМ ====================
import { state } from '../core/state.js';
import { isTgWebApp } from './start-menu.js';
import { showToast } from './start-menu.js';
export function initShare() {
// Обработчик клика на worldId для копирования ссылки
document.getElementById('worldId').onclick = () => {
const shareUrl = new URL(window.location.href);
shareUrl.searchParams.set('world', state.worldId);
const shareUrlString = shareUrl.toString();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrlString).then(() => {
alert('Ссылка скопирована!');
}).catch(() => {
alert('Ссылка на мир:\n' + shareUrlString);
});
} else {
alert('Ссылка на мир:\n' + shareUrlString);
}
};
// Keep worldId click-to-copy as before
const worldIdEl = document.getElementById('worldId');
if (worldIdEl) {
worldIdEl.onclick = () => shareWorld();
}
}
export function shareWorld() {
const shareUrl = new URL(window.location.href);
shareUrl.searchParams.set('world', state.worldId);
const shareUrlString = shareUrl.toString();
const url = shareUrl.toString();
// Копируем в буфер обмена
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(shareUrlString).then(() => {
alert('Ссылка скопирована!');
}).catch(() => {
alert('Ссылка на мир:\n' + shareUrlString);
});
} else {
alert('Ссылка на мир:\n' + shareUrlString);
// In Telegram WebApp — use native share
if (isTgWebApp && window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.switchInlineQuery) {
const inviteUrl = 'https://t.me/' + (state.TELEGRAM_BOT_USERNAME || 'grechka_mkn8n_bot') + '/app?startapp=' + encodeURIComponent(state.worldId);
Telegram.WebApp.openTelegramLink(
'https://t.me/share/url?url=' + encodeURIComponent(inviteUrl) +
'&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Код мира: ' + state.worldId)
);
return;
}
}
// Otherwise — show custom modal with URL + copy button
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:100000;display:flex;align-items:center;justify-content:center;';
const box = document.createElement('div');
box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:20px;max-width:400px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;';
const title = document.createElement('div');
title.textContent = '📤 Пригласить друзей';
title.style.cssText = 'font-size:18px;font-weight:bold;margin-bottom:12px;color:#222;';
box.appendChild(title);
const codeLabel = document.createElement('div');
codeLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;';
codeLabel.textContent = 'Код мира:';
box.appendChild(codeLabel);
const codeBox = document.createElement('div');
codeBox.style.cssText = 'font-size:20px;font-weight:bold;background:#fff;border:3px solid #000;padding:8px 12px;margin-bottom:12px;letter-spacing:2px;text-align:center;color:#5A8F3C;';
codeBox.textContent = state.worldId;
box.appendChild(codeBox);
const linkLabel = document.createElement('div');
linkLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;';
linkLabel.textContent = 'Ссылка:';
box.appendChild(linkLabel);
const linkBox = document.createElement('input');
linkBox.type = 'text';
linkBox.readOnly = true;
linkBox.value = url;
linkBox.style.cssText = 'width:100%;padding:8px;font-family:Courier New,monospace;font-size:13px;background:#fff;border:3px solid #000;color:#222;margin-bottom:12px;box-sizing:border-box;';
linkBox.onclick = () => linkBox.select();
box.appendChild(linkBox);
const btnRow = document.createElement('div');
btnRow.style.cssText = 'display:flex;gap:8px;';
const btnCopy = document.createElement('button');
btnCopy.textContent = '📋 Скопировать';
btnCopy.style.cssText = 'flex:1;padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;';
btnCopy.onclick = () => {
navigator.clipboard.writeText(url).then(() => {
showToast('📋 Ссылка скопирована!');
overlay.remove();
}).catch(() => {
linkBox.select();
document.execCommand('copy');
showToast('📋 Ссылка скопирована!');
overlay.remove();
});
};
btnRow.appendChild(btnCopy);
const btnCode = document.createElement('button');
btnCode.textContent = '📋 Код';
btnCode.style.cssText = 'padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#3C5F9E;color:#fff;box-shadow:2px 2px 0 #000;';
btnCode.onclick = () => {
navigator.clipboard.writeText(state.worldId).then(() => {
showToast('📋 Код скопирован: ' + state.worldId);
}).catch(() => {
showToast('Код: ' + state.worldId);
});
};
btnRow.appendChild(btnCode);
const btnClose = document.createElement('button');
btnClose.textContent = '✕';
btnClose.style.cssText = 'padding:10px 14px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#9E2B2B;color:#fff;box-shadow:2px 2px 0 #000;';
btnClose.onclick = () => overlay.remove();
btnRow.appendChild(btnClose);
box.appendChild(btnRow);
overlay.appendChild(box);
// Close on click outside box
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
document.body.appendChild(overlay);
}

586
src/ui/start-menu.js Normal file
View File

@ -0,0 +1,586 @@
// ==================== START MENU ====================
import { state } from '../core/state.js';
// Detect Telegram WebApp
const isTgWebApp = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData);
let tgUser = null;
let tgSaveLoaded = false;
if (isTgWebApp) {
try {
Telegram.WebApp.ready();
Telegram.WebApp.expand();
tgUser = Telegram.WebApp.initDataUnsafe?.user || null;
console.log('[TG] Mini App detected, user:', tgUser?.id, tgUser?.username);
// Set player name from TG
if (tgUser && tgUser.first_name) {
const tgName = tgUser.username || tgUser.first_name;
localStorage.setItem('minegrechka_playerName', tgName);
}
} catch (e) {
console.warn('[TG] WebApp init error:', e);
}
}
// Export TG info for other modules
export { isTgWebApp, tgUser };
// ==================== HELPERS ====================
function generateShortWorldCode() {
// Generate readable 6-char code: consonants + digits (easy to copy/share)
const c = 'bcdfghjkmnpqrstvwxyz';
const d = '23456789';
let code = '';
for (let i = 0; i < 3; i++) code += c[Math.floor(Math.random() * c.length)];
for (let i = 0; i < 3; i++) code += d[Math.floor(Math.random() * d.length)];
return code;
}
export function showToast(msg, duration) {
duration = duration || 1800;
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;';
document.body.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration);
}
function customAlert(msg) {
const overlay = document.createElement('div');
overlay.className = 'custom-modal-overlay';
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:100000;';
const box = document.createElement('div');
box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:24px;max-width:360px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;';
const text = document.createElement('div');
text.textContent = msg;
text.style.marginBottom = '16px';
text.style.fontWeight = 'bold';
text.style.fontSize = '16px';
const btn = document.createElement('button');
btn.textContent = 'OK';
btn.style.cssText = 'padding:8px 24px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;';
btn.onclick = () => overlay.remove();
box.appendChild(text);
box.appendChild(btn);
overlay.appendChild(box);
document.body.appendChild(overlay);
}
// ==================== MAIN START MENU ====================
// Minecraft-style CSS for the start menu
const mcCSS = `
.mc-overlay { position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999;
display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace;
image-rendering:pixelated; }
.mc-bg { position:absolute;inset:0;background:#000;opacity:0.75; }
.mc-dirt { position:absolute;bottom:0;left:0;right:0;height:40%;
background:repeating-linear-gradient(0deg,#8B6914 0px,#8B6914 2px,#7A5B12 2px,#7A5B12 4px);opacity:0.3; }
.mc-box { position:relative;z-index:1;background:#C6C6C6;border:4px solid #000;
padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000;max-height:90vh;display:flex;flex-direction:column; }
.mc-titlebar { background:#1a1a2e;padding:12px 16px;text-align:center;
border-bottom:4px solid #000; }
.mc-title { font-size:24px;font-weight:bold;color:#FFAA00;text-shadow:2px 2px 0 #000;
letter-spacing:2px; }
.mc-subtitle { font-size:13px;color:#AAA;margin-top:4px; }
.mc-body { padding:16px;background:#C6C6C6;max-height:80vh;overflow-y:auto;-webkit-overflow-scrolling:touch; }
.mc-btn { display:block;width:100%;padding:10px 16px;margin-bottom:8px;
font-family:'Courier New',monospace;font-size:16px;font-weight:bold;
cursor:pointer;text-align:center;border:3px solid #000;
image-rendering:pixelated;box-shadow:3px 3px 0 #000;
transition:transform 0.05s,box-shadow 0.05s; }
.mc-btn:active { transform:translate(2px,2px);box-shadow:1px 1px 0 #000; }
.mc-btn-green { background:#5A8F3C;color:#E0E0E0;border-color:#3A5F1C;text-shadow:1px 1px 0 #222; }
.mc-btn-green:hover { background:#6DAF4E; }
.mc-btn-blue { background:#3C5F9E;color:#E0E0E0;border-color:#2A4070;text-shadow:1px 1px 0 #222; }
.mc-btn-blue:hover { background:#4B73BE; }
.mc-btn-purple { background:#7B3FA0;color:#E0E0E0;border-color:#5A2C78;text-shadow:1px 1px 0 #222; }
.mc-btn-purple:hover { background:#9555BE; }
.mc-btn-orange { background:#B86E00;color:#E0E0E0;border-color:#8A5200;text-shadow:1px 1px 0 #222; }
.mc-btn-orange:hover { background:#DA8500; }
.mc-btn-red { background:#9E2B2B;color:#E0E0E0;border-color:#6E1B1B;text-shadow:1px 1px 0 #222; }
.mc-btn-red:hover { background:#BE3B3B; }
.mc-btn-gray { background:#555;color:#AAA;border-color:#333;text-shadow:1px 1px 0 #111; }
.mc-btn-gray:hover { background:#666;color:#DDD; }
.mc-btn-outline { background:transparent;color:#555;border-color:#555;
text-shadow:none;box-shadow:2px 2px 0 #333; }
.mc-btn-outline:hover { background:#DDD;color:#222; }
.mc-input { width:100%;padding:8px 12px;font-family:'Courier New',monospace;
font-size:16px;background:#FFF;border:3px solid #000;color:#222;
box-shadow:inset 2px 2px 0 #999;margin:4px 0;box-sizing:border-box; }
.mc-input:focus { outline:none;border-color:#FFAA00; }
.mc-label { font-size:13px;color:#444;font-weight:bold;margin:8px 0 4px; }
.mc-divider { border:none;border-top:2px solid #999;margin:12px 0; }
.mc-friend-row { display:flex;justify-content:space-between;align-items:center;
padding:6px 8px;margin-bottom:4px;background:#E8E8E8;border:2px solid #999;
box-shadow:2px 2px 0 #999; }
.mc-friend-btn { padding:4px 10px;font-family:'Courier New',monospace;
font-size:13px;font-weight:bold;cursor:pointer;border:2px solid #000;
box-shadow:1px 1px 0 #000; }
.mc-friend-btn:active { transform:translate(1px,1px);box-shadow:none; }
.mc-friend-btn-green { background:#5A8F3C;color:#E0E0E0; }
.mc-friend-btn-red { background:#9E2B2B;color:#E0E0E0; }
.mc-checkbox-row { display:flex;align-items:center;gap:8px;font-size:13px;color:#444;cursor:pointer; }
.mc-checkbox { width:18px;height:18px;accent-color:#5A8F3C; }
`;
export function hideBoot() {
var b = document.getElementById('boot');
if (b) b.remove();
}
export function showStartMenu() {
return new Promise(async (resolve) => {
const urlParams = new URLSearchParams(window.location.search);
const worldParam = urlParams.get('world');
const tgStartParam = (isTgWebApp && Telegram.WebApp.initDataUnsafe?.start_param)
? Telegram.WebApp.initDataUnsafe.start_param.trim()
: null;
// If world is in URL or TG start param — skip menu, auto-join.
// `default` is NOT a playable world: the server returns blocks:[] for it,
// so auto-joining leaves the player staring at empty sky with no menu to
// escape to. Treat it as "no world chosen" and show the menu instead.
const RESERVED_WORLD_IDS = new Set(['', 'default', 'lobby', 'null', 'undefined']);
if ((worldParam && worldParam.trim() !== '' && !RESERVED_WORLD_IDS.has(worldParam.trim())) || tgStartParam) {
const worldId = (worldParam && worldParam.trim()) || tgStartParam;
console.log('[Menu] Joining world from URL/startParam:', worldId);
// Set player name from TG or localStorage or default
if (!state.playerName) {
if (isTgWebApp && tgUser) {
state.playerName = tgUser.username || tgUser.first_name || 'Игрок';
} else {
state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок';
}
localStorage.setItem('minegrechka_playerName', state.playerName);
}
hideBoot();
resolve(worldId);
return;
}
// Inject CSS
const styleEl = document.createElement('style');
styleEl.textContent = mcCSS;
document.head.appendChild(styleEl);
// Build overlay
const overlay = document.createElement('div');
overlay.className = 'mc-overlay';
overlay.id = 'startMenu';
overlay.innerHTML = '<div class="mc-bg"></div><div class="mc-dirt"></div>';
const box = document.createElement('div');
box.className = 'mc-box';
// Title bar
const titleBar = document.createElement('div');
titleBar.className = 'mc-titlebar';
titleBar.innerHTML = '<div class="mc-title">⛏️ GrechkaCraft</div>' +
'<div class="mc-subtitle">' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '</div>';
box.appendChild(titleBar);
const body = document.createElement('div');
body.className = 'mc-body';
// Mutable worldId holder
let selectedWorldId = null;
// === Buttons ===
// 🌍 New World
const btnNew = document.createElement('button');
btnNew.className = 'mc-btn mc-btn-green';
btnNew.textContent = '🌍 Новый мир';
btnNew.onclick = () => {
selectedWorldId = generateShortWorldCode();
overlay.remove();
styleEl.remove();
resolve(selectedWorldId);
};
body.appendChild(btnNew);
// 🔗 Join by code
const btnJoin = document.createElement('button');
btnJoin.className = 'mc-btn mc-btn-blue';
btnJoin.textContent = '🔗 Вступить по коду';
btnJoin.onclick = () => {
body.innerHTML = '';
const lbl = document.createElement('div');
lbl.className = 'mc-label';
lbl.textContent = 'Код мира:';
body.appendChild(lbl);
const codeInput = document.createElement('input');
codeInput.className = 'mc-input';
codeInput.type = 'text';
codeInput.placeholder = 'bkh237';
codeInput.maxLength = 12;
body.appendChild(codeInput);
const btnGo = document.createElement('button');
btnGo.className = 'mc-btn mc-btn-blue';
btnGo.textContent = 'Войти →';
btnGo.onclick = () => {
const code = codeInput.value.trim().toLowerCase();
if (code.length >= 3) {
selectedWorldId = code;
overlay.remove();
styleEl.remove();
resolve(selectedWorldId);
}
};
body.appendChild(btnGo);
const btnBack = document.createElement('button');
btnBack.className = 'mc-btn mc-btn-gray';
btnBack.textContent = '← Назад';
btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); };
body.appendChild(btnBack);
codeInput.focus();
codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); });
};
body.appendChild(btnJoin);
// 💾 Continue saved game
const hasLocalSave = !!localStorage.getItem(state.SAVE_KEY || 'minegrechka_save');
if (isTgWebApp || hasLocalSave) {
const btnHistory = document.createElement('button');
btnHistory.className = 'mc-btn mc-btn-purple';
btnHistory.textContent = '💾 Продолжить игру';
btnHistory.onclick = async () => {
if (isTgWebApp && tgUser) {
try {
const resp = await fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id);
const data = await resp.json();
if (data.ok && data.world_id) {
selectedWorldId = data.world_id;
tgSaveLoaded = true;
overlay.remove();
styleEl.remove();
resolve(selectedWorldId);
return;
}
} catch (e) { console.warn('[TG] Load save failed:', e); }
}
if (hasLocalSave) {
// Continue with a new world code (the save will be loaded separately by initDB/loadGame)
selectedWorldId = generateShortWorldCode();
overlay.remove();
styleEl.remove();
resolve(selectedWorldId);
return;
}
customAlert('Сохранений не найдено');
};
body.appendChild(btnHistory);
}
// === TG-only buttons ===
if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) {
// 📤 Invite friends
const btnInvite = document.createElement('button');
btnInvite.className = 'mc-btn mc-btn-outline';
btnInvite.style.marginTop = '16px';
btnInvite.textContent = '📤 Пригласить друзей';
btnInvite.onclick = () => {
const code = selectedWorldId || generateShortWorldCode();
const url = 'https://t.me/' + state.TELEGRAM_BOT_USERNAME + '/app?startapp=' + encodeURIComponent(code);
if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) {
Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code));
} else {
navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url));
}
};
body.appendChild(btnInvite);
// 👥 Friends
const btnFriends = document.createElement('button');
btnFriends.className = 'mc-btn mc-btn-orange';
btnFriends.style.marginTop = '8px';
btnFriends.textContent = '👥 Друзья';
btnFriends.onclick = async () => {
try {
const resp = await fetch(state.SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
const data = await resp.json();
body.innerHTML = '';
// Back button at top
const btnBackTop = document.createElement('button');
btnBackTop.className = 'mc-btn mc-btn-gray';
btnBackTop.textContent = '← Назад';
btnBackTop.style.marginBottom = '12px';
btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); };
body.appendChild(btnBackTop);
// Nickname section
if (data.nickname) {
const nickDiv = document.createElement('div');
nickDiv.className = 'mc-friend-row';
nickDiv.innerHTML = '🔗 Твой ник: <b>' + data.nickname + '</b>';
nickDiv.style.background = '#D5E8C5';
body.appendChild(nickDiv);
} else {
const nickLabel = document.createElement('div');
nickLabel.className = 'mc-label';
nickLabel.style.color = '#9E2B2B';
nickLabel.textContent = '⚡ Задай ник, чтобы друзья могли тебя найти:';
body.appendChild(nickLabel);
const nickRow = document.createElement('div');
nickRow.style.display = 'flex';
nickRow.style.gap = '4px';
nickRow.style.marginBottom = '8px';
const nickInput = document.createElement('input');
nickInput.className = 'mc-input';
nickInput.style.flex = '1';
nickInput.type = 'text';
nickInput.maxLength = 16;
nickInput.placeholder = 'Твой ник';
const nickBtn = document.createElement('button');
nickBtn.className = 'mc-btn mc-btn-green';
nickBtn.style.width = 'auto';
nickBtn.style.marginBottom = '0';
nickBtn.style.padding = '8px 16px';
nickBtn.textContent = '✓';
nickBtn.onclick = async () => {
const n = nickInput.value.trim().toLowerCase();
if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; }
const check = await fetch(state.SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n));
const cd = await check.json();
if (!cd.available) { showToast('❌ Ник занят'); return; }
const setResp = await fetch(state.SERVER_URL + '/api/nick/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tg_id: tgUser.id, nickname: n })
});
const sd = await setResp.json();
if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
};
nickRow.appendChild(nickInput);
nickRow.appendChild(nickBtn);
body.appendChild(nickRow);
}
// Friends list
if (data.friends && data.friends.length) {
const listLabel = document.createElement('div');
listLabel.className = 'mc-label';
listLabel.textContent = '👥 Друзья:';
body.appendChild(listLabel);
for (const f of data.friends) {
const fDiv = document.createElement('div');
fDiv.className = 'mc-friend-row';
const statusDot = f.online ? '🟢' : '⚫';
let info = statusDot + ' <b>' + f.nickname + '</b>';
if (f.online && f.world_id) info += ' — мир ' + f.world_id;
const fInfo = document.createElement('div');
fInfo.innerHTML = info;
fDiv.appendChild(fInfo);
if (f.online && f.world_id) {
const joinBtn = document.createElement('button');
joinBtn.className = 'mc-friend-btn mc-friend-btn-green';
joinBtn.textContent = '🎮 Вступить';
joinBtn.onclick = () => {
selectedWorldId = f.world_id;
overlay.remove();
styleEl.remove();
resolve(selectedWorldId);
};
fDiv.appendChild(joinBtn);
}
body.appendChild(fDiv);
}
} else if (data.friends && !data.friends.length) {
const noFriends = document.createElement('div');
noFriends.className = 'mc-label';
noFriends.style.color = '#888';
noFriends.textContent = 'Пока нет друзей';
body.appendChild(noFriends);
}
// Pending friend requests
if (data.pending && data.pending.length) {
const pLabel = document.createElement('div');
pLabel.className = 'mc-label';
pLabel.style.color = '#B86E00';
pLabel.textContent = '📨 Запросы в друзья:';
body.appendChild(pLabel);
for (const p of data.pending) {
const pItem = document.createElement('div');
pItem.className = 'mc-friend-row';
const span = document.createElement('span');
span.textContent = p.from;
pItem.appendChild(span);
const btns = document.createElement('div');
btns.style.display = 'flex';
btns.style.gap = '4px';
const acceptBtn = document.createElement('button');
acceptBtn.className = 'mc-friend-btn mc-friend-btn-green';
acceptBtn.textContent = '✓';
acceptBtn.onclick = async () => {
const r = await fetch(state.SERVER_URL + '/api/friends/accept', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id })
});
const d = await r.json();
if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
};
const declineBtn = document.createElement('button');
declineBtn.className = 'mc-friend-btn mc-friend-btn-red';
declineBtn.textContent = '✕';
declineBtn.onclick = async () => {
await fetch(state.SERVER_URL + '/api/friends/decline', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id })
});
showToast('Отклонено');
btnFriends.onclick();
};
btns.appendChild(acceptBtn);
btns.appendChild(declineBtn);
pItem.appendChild(btns);
body.appendChild(pItem);
}
}
// Add friend
body.appendChild(document.createElement('hr'));
const addLabel = document.createElement('div');
addLabel.className = 'mc-label';
addLabel.textContent = '➕ Добавить друга по нику:';
body.appendChild(addLabel);
const addRow = document.createElement('div');
addRow.style.display = 'flex';
addRow.style.gap = '4px';
const addInput = document.createElement('input');
addInput.className = 'mc-input';
addInput.style.flex = '1';
addInput.type = 'text';
addInput.maxLength = 16;
addInput.placeholder = 'Ник друга';
const addBtn = document.createElement('button');
addBtn.className = 'mc-btn mc-btn-blue';
addBtn.style.width = 'auto';
addBtn.style.marginBottom = '0';
addBtn.style.padding = '8px 16px';
addBtn.textContent = '+';
addBtn.onclick = async () => {
const nick = addInput.value.trim().toLowerCase();
if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; }
const r = await fetch(state.SERVER_URL + '/api/friends/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tg_id: tgUser.id, nickname: nick })
});
const d = await r.json();
if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); }
else { showToast('❌ ' + (d.error || 'Ошибка')); }
};
addRow.appendChild(addInput);
addRow.appendChild(addBtn);
body.appendChild(addRow);
// Privacy checkboxes
const settings = data.settings || { allow_requests: 1, notify_online: 1 };
const settingsDiv = document.createElement('div');
settingsDiv.style.marginTop = '12px';
const reqLabel = document.createElement('label');
reqLabel.className = 'mc-checkbox-row';
const reqCb = document.createElement('input');
reqCb.className = 'mc-checkbox';
reqCb.type = 'checkbox';
reqCb.checked = settings.allow_requests;
reqCb.onchange = async () => {
await fetch(state.SERVER_URL + '/api/friends/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tg_id: tgUser.id,
allow_requests: reqCb.checked ? 1 : 0,
notify_online: settings.notify_online
})
});
};
reqLabel.appendChild(reqCb);
reqLabel.appendChild(document.createTextNode(' Запросы в друзья'));
settingsDiv.appendChild(reqLabel);
body.appendChild(settingsDiv);
} catch (e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); }
};
body.appendChild(btnFriends);
}
// Player name input — always visible, with Play button
{
const nameDivider = document.createElement('hr');
nameDivider.className = 'mc-divider';
body.appendChild(nameDivider);
const nameLabel = document.createElement('div');
nameLabel.className = 'mc-label';
nameLabel.textContent = 'Твоё имя:';
body.appendChild(nameLabel);
const nameRow = document.createElement('div');
nameRow.style.cssText = 'display:flex;gap:6px;';
const nameInput = document.createElement('input');
nameInput.className = 'mc-input';
nameInput.style.flex = '1';
nameInput.type = 'text';
nameInput.placeholder = 'Игрок';
nameInput.value = state.playerName || localStorage.getItem('minegrechka_playerName') || (tgUser ? (tgUser.username || tgUser.first_name || '') : '');
nameInput.maxLength = 20;
nameRow.appendChild(nameInput);
const playBtn = document.createElement('button');
playBtn.className = 'mc-btn mc-btn-green';
playBtn.style.cssText = 'width:auto;margin-bottom:0;padding:10px 20px;white-space:nowrap;';
playBtn.textContent = '▶ Играть';
playBtn.onclick = () => {
const name = nameInput.value.trim() || 'Игрок';
state.playerName = name;
localStorage.setItem('minegrechka_playerName', name);
// If no worldId yet, generate one and start
if (!selectedWorldId && !state.worldId) {
selectedWorldId = generateShortWorldCode();
}
overlay.remove();
styleEl.remove();
resolve(selectedWorldId || state.worldId);
};
nameRow.appendChild(playBtn);
body.appendChild(nameRow);
// Allow Enter key to start game
nameInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') playBtn.click();
});
// Auto-focus input on mobile
setTimeout(() => nameInput.focus(), 100);
}
box.appendChild(body);
overlay.appendChild(box);
document.body.appendChild(overlay);
hideBoot();
// Set default player name if still empty
if (!state.playerName) {
state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок';
localStorage.setItem('minegrechka_playerName', state.playerName);
}
// Auto-load TG save info (background fetch)
if (isTgWebApp && tgUser && !worldParam) {
fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id)
.then(r => r.json())
.then(data => {
if (data.ok && data.world_id) {
// could update UI but menu is already shown
}
})
.catch(() => {});
}
});
}

277
src/ui/trade.js Normal file
View File

@ -0,0 +1,277 @@
// Trade UI — личная торговля (лицом к лицу)
import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { playSound } from '../audio/sound-engine.js';
import { addChatMessage } from './chat.js';
function itemName(id) {
return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id;
}
function itemIcon(id) {
if (BLOCKS[id]) return '🟫';
if (TOOLS[id]) return TOOLS[id].icon || '🔧';
if (ITEMS[id]) return ITEMS[id].icon || '📦';
return '📦';
}
let tradeOpen = false;
let currentTradeId = null;
let myOffer = {};
let partnerOffer = {};
let iConfirmed = false;
export function isTradeOpen() { return tradeOpen; }
export function initTradeSocket(socket) {
// Incoming trade request
socket.on('trade_request', (data) => {
playSound('click');
// Show trade request modal
const overlay = document.createElement('div');
overlay.id = 'tradeRequestOverlay';
overlay.className = 'custom-modal-overlay';
overlay.innerHTML = `
<div class="custom-modal-box">
<div style="font-size:18px;margin-bottom:12px;">🤝 ${data.from_name} хочет торговать</div>
<div class="modal-btns">
<button id="tradeAcceptBtn" style="background:#2ecc71;padding:10px 20px;border:none;border-radius:10px;color:#fff;font-weight:900;font-size:15px;cursor:pointer;">Принять</button>
<button id="tradeDeclineBtn" style="background:#e74c3c;padding:10px 20px;border:none;border-radius:10px;color:#fff;font-weight:900;font-size:15px;cursor:pointer;">Отклонить</button>
</div>
</div>`;
document.getElementById('game').appendChild(overlay);
document.getElementById('tradeAcceptBtn').addEventListener('click', () => {
overlay.remove();
socket.emit('trade_accept', { from_socket_id: data.from });
});
document.getElementById('tradeDeclineBtn').addEventListener('click', () => {
overlay.remove();
socket.emit('trade_decline', { from_socket_id: data.from });
});
});
socket.on('trade_start', (data) => {
currentTradeId = data.trade_id;
myOffer = {};
partnerOffer = {};
iConfirmed = false;
tradeOpen = true;
renderTradeUI(data.partner_name);
state.tradePanel.style.display = 'block';
});
socket.on('trade_update', (data) => {
partnerOffer = data.player1Offer || data.player2Offer || {};
// Determine which offer is the partner's
const isP1 = state.socket && state.socket.id === data.player1Offer;
// The partner's offer is the one we didn't send
// Actually the server sends both — we figure out which is ours
// Simpler: just use the data directly
if (state.mySocketId) {
// We need to know which offer in the update is ours vs partner's
// The trade_update event doesn't tell us, but we know myOffer already
// So partnerOffer = the one that's not myOffer
}
renderTradeOffers();
});
socket.on('trade_status', (data) => {
// Update confirm buttons
const confirmBtn = document.getElementById('tradeConfirmBtn');
if (confirmBtn) {
const amIPlayer1 = true; // We need to track this
if (iConfirmed) {
confirmBtn.textContent = '✅ Ожидание...';
confirmBtn.disabled = true;
confirmBtn.style.background = '#555';
}
}
// Show partner confirmation status
const partnerStatus = document.getElementById('partnerConfirmStatus');
if (partnerStatus) {
const partnerConfirmed = data.player1Confirm || data.player2Confirm;
partnerStatus.textContent = partnerConfirmed ? '✅ Подтвердил' : '⏳ Ещё не подтвердил';
}
});
socket.on('trade_complete', (data) => {
// Add received items to inventory
const received = data.received || {};
for (const [item, qty] of Object.entries(received)) {
if (qty > 0) {
state.inv[item] = (state.inv[item] || 0) + qty;
}
}
// Remove sent items from inventory
const sent = data.sent || {};
for (const [item, qty] of Object.entries(sent)) {
if (qty > 0) {
state.inv[item] = (state.inv[item] || 0) - qty;
if (state.inv[item] < 0) state.inv[item] = 0;
}
}
addChatMessage('Система', '🤝 Сделка завершена!');
closeTrade();
});
socket.on('trade_declined', () => {
addChatMessage('Система', 'Торговля отклонена');
closeTrade();
});
socket.on('trade_cancelled', () => {
addChatMessage('Система', 'Торговля отменена');
closeTrade();
});
socket.on('trade_error', (data) => {
addChatMessage('Система', '❌ ' + data.msg);
});
}
function renderTradeUI(partnerName) {
const panel = state.tradePanel;
const content = state.tradeContent;
content.innerHTML = `
<div style="text-align:center;color:#f1c40f;font-weight:900;font-size:16px;margin-bottom:8px;">🤝 Торговля с ${partnerName}</div>
<div style="display:flex;gap:12px;">
<div id="myOfferArea" style="flex:1;">
<div style="color:#2ecc71;font-weight:900;font-size:13px;margin-bottom:4px;">Вы предлагаете:</div>
<div id="myOfferItems" style="min-height:40px;padding:6px;border-radius:8px;background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);"></div>
<button id="tradeAddItemBtn" style="width:100%;margin-top:6px;padding:6px;background:#3498db;color:#fff;border:none;border-radius:6px;font-weight:700;cursor:pointer;">+ Добавить</button>
</div>
<div style="display:flex;align-items:center;font-size:24px;">⇄</div>
<div id="partnerOfferArea" style="flex:1;">
<div style="color:#e74c3c;font-weight:900;font-size:13px;margin-bottom:4px;">Партнёр предлагает:</div>
<div id="partnerOfferItems" style="min-height:40px;padding:6px;border-radius:8px;background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);">
<span style="color:#888;font-size:12px;">Ожидание...</span>
</div>
</div>
</div>
<div id="partnerConfirmStatus" style="color:#bbb;font-size:11px;text-align:center;margin-top:6px;">⏳ Партнёр ещё не подтвердил</div>
<div style="display:flex;gap:8px;margin-top:10px;">
<button id="tradeConfirmBtn" style="flex:1;padding:8px;background:#2ecc71;color:#fff;border:none;border-radius:8px;font-weight:900;cursor:pointer;">✅ Подтвердить</button>
<button id="tradeCancelBtn" style="flex:1;padding:8px;background:#e74c3c;color:#fff;border:none;border-radius:8px;font-weight:900;cursor:pointer;">✕ Отмена</button>
</div>`;
document.getElementById('tradeAddItemBtn')?.addEventListener('click', showAddItemDialog);
document.getElementById('tradeConfirmBtn')?.addEventListener('click', confirmTrade);
document.getElementById('tradeCancelBtn')?.addEventListener('click', cancelTrade);
}
function renderTradeOffers() {
const myItems = document.getElementById('myOfferItems');
if (myItems) {
const keys = Object.keys(myOffer).filter(k => myOffer[k] > 0);
if (keys.length === 0) {
myItems.innerHTML = '<span style="color:#888;font-size:12px;">Ничего</span>';
} else {
myItems.innerHTML = keys.map(k => `<div style="color:#fff;font-size:12px;">${itemIcon(k)} ${itemName(k)} x${myOffer[k]}</div>`).join('');
}
}
}
function showAddItemDialog() {
const inv = state.inv;
const items = Object.keys(inv).filter(id => inv[id] > 0);
if (items.length === 0) {
addChatMessage('Система', 'Инвентарь пуст!');
return;
}
const overlay = document.createElement('div');
overlay.id = 'tradeItemPicker';
overlay.className = 'custom-modal-overlay';
let html = '<div class="custom-modal-box" style="max-height:80vh;overflow-y:auto;max-width:360px;"><div style="font-weight:900;font-size:16px;margin-bottom:8px;">Что предложить?</div>';
for (const id of items) {
// Show already offered count
const offered = myOffer[id] || 0;
const available = inv[id] - offered;
if (available <= 0) continue;
html += `<div style="display:flex;align-items:center;gap:8px;padding:6px;border-radius:6px;cursor:pointer;border:1px solid rgba(255,255,255,0.1);margin-bottom:4px;" class="trade-pick-item" data-item="${id}">
<span style="font-size:18px;">${itemIcon(id)}</span>
<span style="flex:1;color:#fff;font-size:13px;">${itemName(id)}</span>
<span style="color:#bbb;font-size:11px;">x${available}</span>
</div>`;
}
html += '</div></div>';
overlay.innerHTML = html;
document.getElementById('game').appendChild(overlay);
overlay.querySelectorAll('.trade-pick-item').forEach(el => {
el.addEventListener('click', () => {
const itemId = el.dataset.item;
// Ask quantity
const available = inv[itemId] - (myOffer[itemId] || 0);
const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1);
if (qty > 0) {
myOffer[itemId] = (myOffer[itemId] || 0) + qty;
renderTradeOffers();
// Reset confirm when offer changes
iConfirmed = false;
const btn = document.getElementById('tradeConfirmBtn');
if (btn) { btn.textContent = '✅ Подтвердить'; btn.disabled = false; btn.style.background = '#2ecc71'; }
}
overlay.remove();
});
});
}
function promptForQty(itemId, max) {
// Simple: just use 1 for now, can enhance later with a proper dialog
const val = parseInt(prompt(`Сколько ${itemName(itemId)}? (макс: ${max})`, '1'));
return isNaN(val) || val < 1 ? 1 : Math.min(val, max);
}
function confirmTrade() {
if (!currentTradeId || !state.socket) return;
// Deduct offered items from inventory
for (const [item, qty] of Object.entries(myOffer)) {
if (qty > 0) {
if (state.inv[item] < qty) {
addChatMessage('Система', `Недостаточно ${itemName(item)}!`);
return;
}
}
}
// Send offer + confirm
state.socket.emit('trade_offer', { trade_id: currentTradeId, offer: myOffer });
// Small delay then confirm
setTimeout(() => {
state.socket.emit('trade_confirm', { trade_id: currentTradeId });
iConfirmed = true;
const btn = document.getElementById('tradeConfirmBtn');
if (btn) { btn.textContent = '✅ Ожидание...'; btn.disabled = true; btn.style.background = '#555'; }
}, 200);
}
function cancelTrade() {
if (!currentTradeId || !state.socket) return;
state.socket.emit('trade_cancel', { trade_id: currentTradeId });
closeTrade();
}
export function closeTrade() {
tradeOpen = false;
currentTradeId = null;
myOffer = {};
partnerOffer = {};
iConfirmed = false;
if (state.tradePanel) state.tradePanel.style.display = 'none';
// Remove any pick overlay
document.getElementById('tradeItemPicker')?.remove();
}
export function requestTradeWithPlayer(targetName) {
if (!state.socket || !state.isMultiplayer) {
addChatMessage('Система', 'Торговля доступна только в мультиплеере');
return;
}
state.socket.emit('trade_request', { target_name: targetName });
addChatMessage('Система', `Запрос отправлен ${targetName}...`);
}

View File

@ -5,16 +5,94 @@ import { grid, blocks, setBlock, getBlock, k } from './world-storage.js';
// Генерация (по X, на всю глубину до bedrock)
const generated = state.generated; // Set of gx already generated
// ========== BIOME SYSTEM ==========
export const BIOMES = {
plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, flowers:true, treeChance:0.12 },
desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, flowers:false, treeChance:0 },
tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, flowers:false, treeChance:0.06 },
swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, flowers:false, treeChance:0.10 },
mountains:{ name:'Горы', surface:'stone', subsurface:'stone', trees:false, flowers:false, treeChance:0 }
};
export function getBiome(gx) {
const temp = Math.sin(gx*0.003 + state.worldSeed*0.01)*0.5 + Math.sin(gx*0.007 + state.worldSeed*0.02)*0.3 + 0.5;
const humid = Math.sin(gx*0.004 + state.worldSeed*0.015 + 1000)*0.5 + Math.cos(gx*0.006 + state.worldSeed*0.02 + 2000)*0.3 + 0.5;
const mtVal = Math.sin(gx*0.001 + state.worldSeed*0.005)*0.5 + 0.5;
if (temp > 0.7) return 'desert';
if (temp < 0.3) return 'tundra';
if (humid > 0.7 && temp >= 0.3 && temp <= 0.7) return 'swamp';
if (temp >= 0.5 && temp <= 0.7 && mtVal > 0.75) return 'mountains';
return 'plains';
}
const biomeCache = {};
export function getCachedBiome(gx) {
const chunk = Math.floor(gx / 8);
if (biomeCache[chunk] === undefined) biomeCache[chunk] = getBiome(chunk * 8);
return biomeCache[chunk];
}
// ========== STRUCTURES ==========
function placeStructure(startGx, startGy, pattern) {
for (let dy = 0; dy < pattern.length; dy++) {
for (let dx = 0; dx < pattern[dy].length; dx++) {
const bt = pattern[dy][dx];
if (bt && bt !== 'air') {
setBlock(startGx + dx, startGy + dy, bt);
}
}
}
}
const PYRAMID_PATTERN = [
['sand','sand','sand','sand','sand','sand','sand'],
['sand','stone','stone','stone','stone','stone','sand'],
['sand','stone','air','air','air','stone','sand'],
['sand','stone','air','air','air','stone','sand'],
['sand','stone','air','air','air','stone','sand'],
['sand','sand','stone','stone','stone','sand','sand']
];
const HOUSE_PATTERN = [
['air','planks','planks','planks','planks','air'],
['planks','air','air','air','air','planks'],
['planks','air','torch','air','air','planks'],
['planks','air','air','air','air','planks'],
['planks','planks','air','planks','planks','planks']
];
const HUT_PATTERN = [
['air','wood','wood','wood','air'],
['wood','air','air','air','wood'],
['wood','air','torch','air','wood'],
['moss','moss','air','moss','moss']
];
function surfaceGyAt(gx) {
// базовая поверхность выше уровня воды с вариациями + "горы"
// Используем seed для детерминированной генерации
// Увеличили амплитуду и добавили больше частот для разнообразия
const n1 = Math.sin(gx * 0.025 + state.worldSeed * 0.001) * 8; // крупные горы
const n2 = Math.sin(gx * 0.012 + state.worldSeed * 0.002) * 12; // средние горы
const n3 = Math.sin(gx * 0.006 + state.worldSeed * 0.003) * 6; // мелкие холмы
const n4 = Math.sin(gx * 0.045 + state.worldSeed * 0.004) * 4; // детали
const n5 = Math.cos(gx * 0.018 + state.worldSeed * 0.005) * 5; // дополнительные вариации
const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); // чем меньше gy - тем выше
const biome = getCachedBiome(gx);
const n1 = Math.sin(gx*0.025 + state.worldSeed*0.001)*8;
const n2 = Math.sin(gx*0.012 + state.worldSeed*0.002)*12;
const n3 = Math.sin(gx*0.006 + state.worldSeed*0.003)*6;
const n4 = Math.sin(gx*0.045 + state.worldSeed*0.004)*4;
const n5 = Math.cos(gx*0.018 + state.worldSeed*0.005)*5;
let h;
switch(biome) {
case 'desert':
h = Math.floor(SEA_GY - 4 + n3*0.3 + n4*0.5);
break;
case 'tundra':
h = Math.floor(SEA_GY - 6 + n2*0.5 + n3*0.4 + n5*0.3);
break;
case 'swamp':
h = Math.floor(SEA_GY - 2 + n3*0.2 + n4*0.3);
h = Math.max(h, SEA_GY - 3);
break;
case 'mountains':
h = Math.floor(SEA_GY - 15 + n1*1.5 + n2*1.2 + n3);
break;
default: // plains
h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5);
}
return h;
}
@ -23,37 +101,50 @@ function genColumn(gx) {
generated.add(gx);
const sgy = surfaceGyAt(gx);
const biome = getCachedBiome(gx);
// вода (если поверхность ниже уровня моря => sgy > SEA_GY)
// === Вода и поверхность ===
if (sgy > SEA_GY) {
for (let gy = SEA_GY; gy < sgy; gy++) {
setBlock(gx, gy, 'water');
const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water';
setBlock(gx, gy, blockType);
}
// пляж
setBlock(gx, sgy, 'sand');
} else {
// верхний блок: снег на высоких точках
if (sgy < SEA_GY - 10) setBlock(gx, sgy, 'stone');
else setBlock(gx, sgy, 'grass');
const b = BIOMES[biome];
setBlock(gx, sgy, b.surface);
// болото: случайные лужи болотной воды
if (biome === 'swamp' && seededRandom(gx*3, sgy) < 0.15) {
setBlock(gx, sgy-1, 'swamp_water');
}
// тундра: лёд на воде рядом
if (biome === 'tundra' && sgy === SEA_GY && seededRandom(gx, SEA_GY-1) < 0.3) {
setBlock(gx, SEA_GY-1, 'ice');
}
}
// подповерхностные слои
// === Подповерхностные слои ===
for (let gy = sgy + 1; gy <= BEDROCK_GY; gy++) {
if (gy === BEDROCK_GY) {
setBlock(gx, gy, 'bedrock');
continue;
}
let t = 'stone';
let t = BIOMES[biome].subsurface;
// ближе к поверхности
if (gy <= sgy + 3) t = 'dirt';
// глубже — камень
if (gy > sgy + 3) t = 'stone';
// биомы/материалы
if (sgy > SEA_GY && gy === sgy + 1 && seededRandom(gx, gy) < 0.25) t = 'clay';
if (gy > sgy + 6 && seededRandom(gx, gy) < 0.07) t = 'gravel';
// пустыня: sand глубже
if (biome === 'desert' && gy <= sgy + 6) t = 'sand';
// руды: чем глубже, тем интереснее
// болото: глина ближе к поверхности
if (biome === 'swamp' && gy <= sgy + 2 && seededRandom(gx, gy) < 0.3) t = 'clay';
// горы: gravel
if (biome === 'mountains' && gy > sgy + 4 && seededRandom(gx, gy) < 0.12) t = 'gravel';
// общие руды
const depth = gy - sgy;
const r = seededRandom(gx, gy);
if (t === 'stone') {
@ -67,22 +158,71 @@ function genColumn(gx) {
setBlock(gx, gy, t);
}
// Деревья и цветы (только на траве, и не в воде)
// === Растительность и декор ===
const b = BIOMES[biome];
const top = getBlock(gx, sgy);
if (top && top.t === 'grass') {
if (seededRandom(gx, sgy - 1) < 0.10) {
setBlock(gx, sgy - 1, 'flower');
}
if (seededRandom(gx, sgy - 2) < 0.12) {
// простое дерево
setBlock(gx, sgy - 1, 'wood');
setBlock(gx, sgy - 2, 'wood');
setBlock(gx, sgy - 3, 'leaves');
setBlock(gx - 1, sgy - 3, 'leaves');
setBlock(gx + 1, sgy - 3, 'leaves');
// цветы (только plains)
if (biome === 'plains' && top && top.t === 'grass' && seededRandom(gx, sgy-1) < 0.10) {
setBlock(gx, sgy-1, 'flower');
}
// деревья
if (b.trees && seededRandom(gx*7, sgy-2) < b.treeChance) {
if (biome === 'tundra') {
const th = 3 + Math.floor(seededRandom(gx, sgy) * 3);
for (let i = 0; i < th; i++) setBlock(gx, sgy-1-i, 'wood');
for (let row = 0; row < th; row++) {
const w = Math.min(row + 1, 2);
for (let dx = -w; dx <= w; dx++) {
const ly = sgy-1-th+row;
if (ly >= 0) setBlock(gx+dx, ly, 'spruce_leaves');
}
}
} else if (biome === 'swamp') {
setBlock(gx, sgy-1, 'wood');
setBlock(gx, sgy-2, 'moss');
setBlock(gx-1, sgy-2, 'leaves');
setBlock(gx+1, sgy-2, 'leaves');
} else {
setBlock(gx, sgy-1, 'wood');
setBlock(gx, sgy-2, 'wood');
setBlock(gx, sgy-3, 'leaves');
setBlock(gx-1, sgy-3, 'leaves');
setBlock(gx+1, sgy-3, 'leaves');
}
}
// кактусы (пустыня)
if (biome === 'desert' && seededRandom(gx*11, sgy) < 0.04) {
const ch = 1 + Math.floor(seededRandom(gx, sgy+1) * 2);
for (let i = 0; i < ch; i++) setBlock(gx, sgy-1-i, 'cactus');
}
// грибы (болото)
if (biome === 'swamp' && seededRandom(gx*13, sgy) < 0.06) {
setBlock(gx, sgy-1, 'mushroom');
}
// сухие кусты (пустыня)
if (biome === 'desert' && seededRandom(gx*17, sgy) < 0.05) {
setBlock(gx, sgy-1, 'dead_bush');
}
// === Структуры мира ===
// Пирамида в пустыне
if (biome === 'desert' && ((gx % 200 + 200) % 200) === 47 && sgy < SEA_GY && sgy > SEA_GY - 5) {
placeStructure(gx, sgy - 5, PYRAMID_PATTERN);
}
// Дом в равнине
if (biome === 'plains' && ((gx % 150 + 150) % 150) === 33 && sgy < SEA_GY) {
placeStructure(gx, sgy - 4, HOUSE_PATTERN);
}
// Хижина в болоте
if (biome === 'swamp' && ((gx % 180 + 180) % 180) === 55 && sgy < SEA_GY) {
placeStructure(gx, sgy - 3, HUT_PATTERN);
}
// Применяем серверные оверрайды для этой колонны
const colPrefix = gx + ',';
for (const [key, ov] of state.serverOverrides) {
@ -105,7 +245,6 @@ function genColumn(gx) {
function regenerateVisibleChunks() {
const gx0 = Math.floor(state.camX / TILE);
for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) {
// Принудительно перегенерируем колонну
generated.delete(gx);
genColumn(gx);
}
@ -123,4 +262,4 @@ function seededRandom(gx, gy) {
return n - Math.floor(n);
}
export { generated, surfaceGyAt, genColumn, regenerateVisibleChunks, ensureGenAroundCamera, seededRandom };
export { generated, surfaceGyAt, genColumn, regenerateVisibleChunks, ensureGenAroundCamera, seededRandom, PYRAMID_PATTERN, HOUSE_PATTERN, HUT_PATTERN };

View File

@ -1,6 +1,5 @@
import { TILE, SEA_GY } from '../core/constants.js';
import { state } from '../core/state.js';
import { W, H } from '../core/canvas.js';
import { grid, blocks, k, isSolid } from './world-storage.js';
// Физика жидкости
@ -19,6 +18,8 @@ function updateWaterPhysics(dt) {
// Собираем только видимые водные блоки в очередь (оптимизация)
waterUpdateQueue.clear();
const W = state.W;
const H = state.H;
const minGX = Math.floor(state.camX / TILE) - 10;
const maxGX = Math.floor((state.camX + W) / TILE) + 10;
const minGY = Math.floor(state.camY / TILE) - 10;
@ -123,4 +124,4 @@ function updateWaterPhysics(dt) {
}
}
export { updateWaterPhysics };
export { updateWaterPhysics };

View File

@ -1,29 +1,171 @@
// ==================== ПОГОДА ====================
// ==================== ПОГОДА (biome-aware) ====================
import { state } from '../core/state.js';
import { isNight } from '../entities/mob-ai.js';
import { surfaceGyAt } from './generation.js';
import { getCachedBiome } from './generation.js';
import { SEA_GY } from '../core/constants.js';
// Возможные погоды по биому
const BIOME_WEATHER = {
snow: ['clear', 'snow', 'blizzard'],
tundra: ['clear', 'snow', 'blizzard'],
forest: ['clear', 'rain', 'storm'],
plains: ['clear', 'rain'],
desert: ['clear', 'clear', 'rain'],
swamp: ['clear', 'rain', 'storm'],
mountains: ['clear', 'rain', 'snow']
};
// Помощник интерполяции цветов
function lerpColor(a, b, t) {
const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16);
const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16);
const r = Math.round(ar + (br - ar) * t);
const g = Math.round(ag + (bg - ag) * t);
const bl = Math.round(ab + (bb - ab) * t);
return '#' + ((1<<24)|(r<<16)|(g<<8)|bl).toString(16).slice(1);
}
// Цвета неба по биому, погоде и времени суток
function skyColor(night, biome, weatherType, worldTime) {
function weatherSky() {
switch (weatherType) {
case 'blizzard': return '#8a9bae';
case 'storm': return '#3d4f5f';
case 'rain': return '#6B7B8D';
case 'snow': return '#b8c6d4';
case 'clear': return null;
default: return null;
}
}
const dayColor = (biome === 'desert') ? '#f4d9a0' : (biome === 'tundra' || biome === 'snow') ? '#c4d4e0' : (biome === 'swamp') ? '#4a6741' : '#87CEEB';
const dawnColor = '#FF9940';
const duskMidColor = '#FF6B35';
const duskEndColor = '#1a0533';
const nightColor = '#070816';
let skyColorResult;
if (worldTime < 0.05) {
const t = worldTime / 0.05;
skyColorResult = lerpColor(dawnColor, dayColor, t);
} else if (worldTime < 0.45) {
skyColorResult = dayColor;
} else if (worldTime < 0.55) {
const t = (worldTime - 0.45) / 0.1;
if (t < 0.5) {
skyColorResult = lerpColor(dayColor, duskMidColor, t * 2);
} else {
skyColorResult = lerpColor(duskMidColor, duskEndColor, (t - 0.5) * 2);
}
} else {
skyColorResult = nightColor;
if (weatherType === 'blizzard') return '#1a1a2e';
if (weatherType === 'storm') return '#0a0a15';
return nightColor;
}
const wc = weatherSky();
if (wc) {
const blendT = 0.6;
skyColorResult = lerpColor(skyColorResult, wc, blendT);
}
return skyColorResult;
}
export function updateWeather(dt) {
state.weatherTimer += dt;
if (state.weatherTimer >= state.weatherChangeInterval) {
state.weatherTimer = 0;
state.weatherChangeInterval = 60 + Math.random() * 120;
// Выбираем новую погоду: 40% дождь днём, 25% дождь ночью, остальное ясно
const nightChance = isNight() ? 0.25 : 0.40;
state.isRaining = Math.random() < nightChance;
// Определяем биом по позиции игрока
const playerGx = Math.floor(state.player.x / state.TILE);
const biome = getCachedBiome(playerGx);
const weathers = BIOME_WEATHER[biome] || ['clear'];
const night = isNight();
const weights = weathers.map(w => {
if (w === 'clear') return night ? 2 : 4;
if (w === 'rain') return night ? 3 : 2;
if (w === 'snow') return night ? 2 : 3;
if (w === 'storm') return night ? 3 : 1;
if (w === 'blizzard') return night ? 3 : 1;
return 1;
});
const total = weights.reduce((a, b) => a + b, 0);
let r = Math.random() * total;
state.weatherType = weathers[0];
for (let i = 0; i < weathers.length; i++) {
r -= weights[i];
if (r <= 0) { state.weatherType = weathers[i]; break; }
}
state.isRaining = (state.weatherType === 'rain' || state.weatherType === 'storm');
}
// Плавная интерполяция интенсивности
const target = state.isRaining ? (0.4 + Math.random() * 0.01) : 0;
state.rainIntensity += (target - state.rainIntensity) * dt * 0.5;
// Плавная интерполяция интенсивности дождя
const rainTarget = (state.weatherType === 'rain' || state.weatherType === 'storm') ? 0.6 : 0;
state.rainIntensity += (rainTarget - state.rainIntensity) * dt * 0.5;
if (state.rainIntensity < 0.01) state.rainIntensity = 0;
// Молния (только при storm)
if (state.weatherType === 'storm') {
state.lightningTimer -= dt;
if (state.lightningTimer <= 0) {
state.lightningFlash = 0.25;
state.lightningTimer = 4 + Math.random() * 12;
}
}
if (state.lightningFlash > 0) {
state.lightningFlash -= dt;
if (state.lightningFlash < 0) state.lightningFlash = 0;
}
// Снежинки (при snow / blizzard)
if (state.weatherType === 'snow' || state.weatherType === 'blizzard') {
const spawnCount = state.weatherType === 'blizzard' ? Math.floor(80 * dt) : Math.floor(30 * dt);
const maxFlakes = state.weatherType === 'blizzard' ? 600 : 250;
for (let i = 0; i < spawnCount && state.snowflakes.length < maxFlakes; i++) {
state.snowflakes.push({
x: state.camX + Math.random() * state.W,
y: state.camY - 20,
vy: 40 + Math.random() * 60,
vx: (state.weatherType === 'blizzard' ? -60 - Math.random() * 80 : -10 - Math.random() * 20),
size: 2 + Math.random() * 3,
wobble: Math.random() * Math.PI * 2
});
}
}
// Обновление снежинок
for (let i = state.snowflakes.length - 1; i >= 0; i--) {
const s = state.snowflakes[i];
s.y += s.vy * dt;
s.x += s.vx * dt;
s.wobble += dt * 3;
s.x += Math.sin(s.wobble) * 15 * dt;
if (s.y > state.camY + state.H + 20 || s.x < state.camX - 50 || s.x > state.camX + state.W + 50) {
state.snowflakes.splice(i, 1);
}
}
// Убираем снег при смене погоды
if (state.weatherType !== 'snow' && state.weatherType !== 'blizzard' && !state.isRaining) {
if (state.snowflakes.length > 0) {
state.snowflakes.splice(0, Math.ceil(state.snowflakes.length * dt * 2));
}
}
}
export function updateRain(dt) {
if (!state.isRaining || state.rainIntensity < 0.01) {
state.raindrops.length = 0;
if (state.raindrops.length > 0) {
state.raindrops.splice(0, Math.ceil(state.raindrops.length * dt * 3));
}
return;
}
// Спавн капель
const spawnRate = Math.floor(state.rainIntensity * 60 * dt); // ~60 капель/сек при интенсити=1
const spawnRate = Math.floor(state.rainIntensity * 60 * dt);
for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) {
state.raindrops.push({
x: state.camX + Math.random() * state.W,
@ -32,28 +174,47 @@ export function updateRain(dt) {
len: 8 + Math.random() * 12
});
}
// Обновление
for (let i = state.raindrops.length - 1; i >= 0; i--) {
const d = state.raindrops[i];
d.y += d.vy * dt;
d.x -= 30 * dt; // лёгкий ветер
d.x -= 30 * dt;
if (d.y > state.camY + state.H + 20) {
state.raindrops.splice(i, 1);
}
}
}
export function drawRain() {
if (state.raindrops.length === 0) return;
const ctx = state.ctx;
ctx.save();
ctx.strokeStyle = 'rgba(174,194,224,0.5)';
ctx.lineWidth = 1.5;
ctx.beginPath();
for (const d of state.raindrops) {
ctx.moveTo(d.x, d.y);
ctx.lineTo(d.x - 3, d.y + d.len);
export function drawWeather(ctx) {
if (state.raindrops.length > 0) {
ctx.save();
ctx.strokeStyle = 'rgba(174,194,224,0.5)';
ctx.lineWidth = 1.5;
ctx.beginPath();
for (const d of state.raindrops) {
ctx.moveTo(d.x, d.y);
ctx.lineTo(d.x - 3, d.y + d.len);
}
ctx.stroke();
ctx.restore();
}
ctx.stroke();
ctx.restore();
}
if (state.snowflakes.length > 0) {
ctx.save();
ctx.fillStyle = 'rgba(255,255,255,0.85)';
for (const s of state.snowflakes) {
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
if (state.lightningFlash > 0) {
ctx.save();
ctx.fillStyle = `rgba(255,255,255,${Math.min(1, state.lightningFlash * 4)})`;
ctx.fillRect(0, 0, state.W, state.H);
ctx.restore();
}
}
export { skyColor };

View File

@ -1,9 +1,10 @@
import { BLOCKS } from '../data/blocks.js';
import { state } from '../core/state.js';
// Мир-хранилище
const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...}
const blocks = []; // для рендера/перебора видимых
// Мир-хранилище — используем state.grid и state.blocks для единого источника данных
// Это гарантирует, что все модули (включая loop.js) видят одни и те же данные
const grid = state.grid; // Map: key "gx,gy" => {gx,gy,t, ...}
const blocks = state.blocks; // Array для рендера/перебора видимых
function k(gx, gy) { return gx + ',' + gy; }
@ -54,4 +55,4 @@ function removeBlock(gx, gy) {
return b;
}
export { grid, blocks, k, getBlock, hasBlock, isSolid, setBlock, removeBlock };
export { grid, blocks, k, getBlock, hasBlock, isSolid, setBlock, removeBlock };

View File

@ -0,0 +1,37 @@
// Звуковой движок
const sounds = {};
export { sounds };
export function loadSound(id, src) {
const audio = new Audio();
audio.src = src;
audio.volume = 0.3;
sounds[id] = audio;
}
// Загрузка звуков
loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3');
loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3');
loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3');
loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3');
loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3');
loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3');
loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3');
loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3');
loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3');
loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3');
loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3');
loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3');
loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3');
loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3');
loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3');
loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3');
loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3');
export function playSound(id) {
if (sounds[id]) {
sounds[id].currentTime = 0;
sounds[id].play().catch(e => console.error('Sound error:', e));
}
}

12
src3d/config.js Normal file
View File

@ -0,0 +1,12 @@
// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ====================
// This module only sets SERVER_URL. Player name and world ID are handled in main.js
// to avoid duplicate prompts.
const urlParams = new URLSearchParams(window.location.search);
export const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru';
// Защита от mixed content
if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) {
console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP');
alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.');
}

31
src3d/core/canvas.js Normal file
View File

@ -0,0 +1,31 @@
// ==================== ХОЛСТ 3D (WebGL) — GrechkaCraft v2 ====================
// Тот же контракт, что canvas.js: выставляет state.W/H, хранит canvas.
// WebGL-контекст создаёт render3d (лениво, при первом кадре).
import { state } from './state.js';
export function initCanvas() {
const gameEl = document.getElementById('game');
const canvas = document.getElementById('c');
const dpr = Math.max(1, window.devicePixelRatio || 1);
function resize() {
state.W = gameEl.clientWidth;
state.H = gameEl.clientHeight;
}
window.addEventListener('resize', resize);
resize();
state.gameEl = gameEl;
state.canvas = canvas;
state.ctx = null;
state.lightC = null;
state.lightCtx = null;
state.dpr = dpr;
}
export function getCanvas() { return state.canvas; }
export function getCtx() { return null; }
export function getLightC() { return null; }
export function getLightCtx() { return null; }

32
src3d/core/constants.js Normal file
View File

@ -0,0 +1,32 @@
// ==================== КОНСТАНТЫ ИГРЫ ====================
export const TILE = 40;
// Мир
export const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов
export const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже
export const GEN_MARGIN_X = 40; // запас генерации по X (в тайлах) — увеличен для плавной прогрузки
// Физика
export const GRAV = 2200;
export const GRAV_WATER = 550;
export const MOVE = 320;
export const JUMP = 760;
// День/ночь
export const DAY_LEN = 360; // замедлил смену дня/ночи в 3 раза
// Сохранение
export const SAVE_KEY = 'minegrechka_save';
// Сеть
export const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду
// Погода
export const MAX_RAINDROPS = 200;
// Чат
export const MAX_CHAT_MESSAGES = 50;
// Голосовой чат
export const VOICE_SERVER = 'wss://voicegrech.mkn8n.ru';

251
src3d/core/state.js Normal file
View File

@ -0,0 +1,251 @@
import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X, GRAV, GRAV_WATER, MOVE, JUMP, DAY_LEN, SAVE_KEY, MOVE_SEND_INTERVAL, MAX_RAINDROPS, MAX_CHAT_MESSAGES, VOICE_SERVER } from './constants.js';
// Все мутабельные переменные игры в одном объекте состояния
export const state = {
// Константы (для доступа из модулей)
TILE,
SEA_GY,
BEDROCK_GY,
GEN_MARGIN_X,
GRAV,
GRAV_WATER,
MOVE,
JUMP,
DAY_LEN,
SAVE_KEY,
MOVE_SEND_INTERVAL,
MAX_RAINDROPS,
MAX_CHAT_MESSAGES,
VOICE_SERVER,
// Камера
camX: 0,
camY: 0,
W: 0,
H: 0,
dpr: 1,
// Canvas/DOM (заполняется в main.js)
gameEl: null,
canvas: null,
ctx: null,
lightC: null,
lightCtx: null,
// День/ночь
worldTime: 0,
isNightTime: false,
dayNightCycle: true,
// Мультиплеер
isMultiplayer: false,
mySocketId: null,
socket: null,
voiceSocket: null,
voicePosT: 0,
// Сервер
SERVER_URL: '',
TELEGRAM_BOT_USERNAME: '',
TELEGRAM_APP_SHORT_NAME: '',
// Игрок
playerName: null,
worldId: null,
// Инвентарь/UI
selected: "dirt",
showFullInventory: false,
craftOpen: false,
inventoryOpen: false,
chatOpen: false,
minimapOpen: false,
modeIdx: 0,
currentFurnaceKey: null,
// Мир
blocksVersion: 0,
worldSeed: Math.floor(Math.random() * 1000000),
// Погода
weatherType: "clear",
isRaining: false,
rainIntensity: 0,
weatherTimer: 0,
weatherChangeInterval: 60 + Math.random() * 120,
snowflakes: [],
lightningFlash: 0,
lightningTimer: 0,
// Мобы/спавн
spawnT: 0,
// Цикл
last: 0,
prevJump: false,
// Сеть — throttle отправки позиции
lastMoveSendTime: 0,
lastSentX: 0,
lastSentY: 0,
// Mining progress
mining: { gx: null, gy: null, progress: 0, hardness: 0 },
// Trade/Market
myTgId: null,
tradePanel: null,
tradeContent: null,
marketPanel: null,
marketContent: null,
// DOM-ссылки (заполняются в main.js)
hpEl: null,
foodEl: null,
sxEl: null,
syEl: null,
todEl: null,
worldIdEl: null,
playerCountEl: null,
deathEl: null,
hotbarEl: null,
craftPanel: null,
recipesEl: null,
inventoryPanel: null,
inventoryGrid: null,
multiplayerStatus: null,
tempEl: null,
// Игрок
player: {
x: 6 * TILE,
y: 0 * TILE,
w: 34,
h: 34,
vx: 0,
vy: 0,
grounded: false,
inWater: false,
headInWater: false,
hp: 100,
hunger: 100,
o2: 100,
invuln: 0,
fallStartY: 0,
lastStepTime: 0,
sleeping: false,
inBoat: false,
armor: 0,
equippedArmor: null,
temperature: 15,
slowTimer: 0
},
// Точка спавна
spawnPoint: { x: 6 * TILE, y: 0 * TILE },
// Инвентарь
inv: {
dirt: 6, stone: 0, sand: 0, gravel: 0, clay: 0,
wood: 0, planks: 0, ladder: 0, leaves: 0, coal: 0,
copper_ore: 0, iron_ore: 0, gold_ore: 0, diamond_ore: 0,
brick: 0, glass: 0,
tnt: 1, campfire: 0, torch: 0,
meat: 0, cooked: 0, arrow: 0,
wood_pickaxe: 0, stone_pickaxe: 0, iron_pickaxe: 0,
wood_sword: 0, stone_sword: 0, iron_sword: 0,
wood_axe: 0, stone_axe: 0, iron_axe: 0,
wood_shovel: 0, stone_shovel: 0, iron_shovel: 0,
iron_armor: 0, gold_armor: 0,
bow: 0, furnace: 0,
bed: 0, boat: 0,
iron_ingot: 0, gold_ingot: 0, copper_ingot: 0,
cobblestone: 0,
// New items
chicken_meat: 0, feather: 0, bone: 0, gunpowder: 0,
wheat: 0, bread: 0, carrot: 0, potato: 0, baked_potato: 0,
mushroom_stew: 0,
scorpion_stinger: 0, polar_fur: 0, slime_ball: 0, eagle_feather: 0,
snow: 0, ice: 0, cactus: 0, mushroom: 0, moss: 0, farmland: 0,
spruce_leaves: 0, dead_bush: 0, swamp_water: 0,
wheat_seeds: 0, wheat_stage0: 0, carrot_stage0: 0, potato_stage0: 0
},
// Лодка
boat: {
x: 0, y: 0,
w: 34, h: 34,
vx: 0, vy: 0,
active: false,
inWater: false
},
// Ввод
inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false },
// Мышь
mouse: { x: 0, y: 0 },
// Другие игроки (MP)
otherPlayers: new Map(),
// Серверные мобы (MP)
serverMobs: new Map(),
// Мобы
mobs: [],
// Снаряды
projectiles: [],
// Отслеживание изменений мира
placedBlocks: [],
removedBlocks: [],
// Серверные изменения
serverOverrides: new Map(),
// Чат
chatMessages: [],
// Погода — капли
raindrops: [],
// Облака
clouds: Array.from({ length: 10 }, () => ({
x: Math.random() * 2000,
y: -200 - Math.random() * 260,
w: 80 + Math.random() * 120,
s: 12 + Math.random() * 20
})),
// Частицы
parts: [],
// Активный TNT
activeTNT: new Set(),
// Прочность инструментов
toolDurability: new Map(),
protections: new Map(),
// Последние выбранные предметы
recentItems: [],
// Активные печи
activeFurnaces: new Map(),
cropTimers: new Map(),
// Сгенерированные колонны
generated: new Set(),
// Изображение героя
heroImg: new Image(),
// Сетка мира
grid: new Map(),
blocks: [],
// In-memory save fallback
inMemorySave: null,
};

55
src3d/data/blocks.js Normal file
View File

@ -0,0 +1,55 @@
export const BLOCKS = {
air: { n:'Воздух', solid:false, hardness:0 },
grass: { n:'Трава', c:'#7cfc00', solid:true, hardness:3, toolType:'shovel' },
dirt: { n:'Грязь', c:'#8b4513', solid:true, hardness:3, toolType:'shovel' },
stone: { n:'Камень', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' },
sand: { n:'Песок', c:'#f4d06f', solid:true, hardness:2, toolType:'shovel' },
gravel: { n:'Гравий', c:'#95a5a6', solid:true, hardness:3, toolType:'shovel' },
clay: { n:'Глина', c:'#74b9ff', solid:true, hardness:3, toolType:'shovel' },
wood: { n:'Дерево', c:'#d35400', solid:true, hardness:4, toolType:'axe' },
planks: { n:'Доски', c:'#e67e22', solid:true, hardness:3, toolType:'axe' },
ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true, hardness:1, toolType:null },
leaves: { n:'Листва', c:'#2ecc71', solid:true, hardness:1, toolType:'axe', minTool:null },
glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55, hardness:1, toolType:null },
water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true, hardness:0 },
coal: { n:'Уголь', c:'#2c3e50', solid:true, hardness:8, toolType:'pickaxe', minTool:'wood' },
copper_ore:{ n:'Медь', c:'#e17055', solid:true, hardness:10, toolType:'pickaxe', minTool:'wood' },
iron_ore: { n:'Железо', c:'#dcdde1', solid:true, hardness:12, toolType:'pickaxe', minTool:'stone' },
iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5, hardness:0 },
gold_ore: { n:'Золото', c:'#f1c40f', solid:true, hardness:15, toolType:'pickaxe', minTool:'stone' },
diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true, hardness:20, toolType:'pickaxe', minTool:'iron' },
brick: { n:'Кирпич', c:'#c0392b', solid:true, hardness:8, toolType:'pickaxe' },
tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true, hardness:1, toolType:null },
campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280, hardness:2, toolType:null },
torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220, hardness:1, toolType:null },
bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true, hardness:Infinity },
flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true, hardness:0 },
bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true, hardness:2, toolType:'axe' },
boat: { n:'Лодка', c:'#8B4513', solid:false, hardness:0 },
furnace: { n:'Печь', c:'#696969', solid:true, smelting:true, hardness:8, toolType:'pickaxe', minTool:'wood' },
cobblestone: { n:'Булыжник', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' },
flag: { n:'Флаг', c:'#e74c3c', solid:true, hardness:1, toolType:'axe', protectionRadius:10 },
// === BIOME BLOCKS ===
snow: { n:'Снег', c:'#ecf0f1', solid:true, hardness:1, toolType:'shovel' },
ice: { n:'Лёд', c:'#74b9ff', solid:true, hardness:1, slip:true, toolType:'pickaxe' },
cactus: { n:'Кактус', c:'#27ae60', solid:true, hardness:1, hurt:true, toolType:null },
mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true, hardness:0 },
moss: { n:'Мох', c:'#0a6640', solid:true, hardness:1, toolType:null },
swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true, hardness:0 },
farmland: { n:'Грядка', c:'#8B6914', solid:true, hardness:1, farmable:true, toolType:'shovel' },
dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true, hardness:0 },
spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true, hardness:1, toolType:'axe' },
// === CROP STAGES ===
wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'wheat_stage1' },
wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'wheat_stage2' },
wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'wheat_stage3' },
wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'wheat', harvestQty:2 },
carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'carrot_stage1' },
carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'carrot_stage2' },
carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'carrot_stage3' },
carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'carrot', harvestQty:3 },
potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'potato_stage1' },
potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'potato_stage2' },
potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'potato_stage3' },
potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'potato', harvestQty:2 },
};

37
src3d/data/items.js Normal file
View File

@ -0,0 +1,37 @@
export const ITEMS = {
meat: { n:'Сырое мясо', icon:'🥩', food:15 },
cooked: { n:'Жареное мясо', icon:'🍖', food:45 },
arrow: { n:'Стрела', icon:'➡️', stack:64 },
chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 },
feather: { n:'Перо', icon:'🪶', stack:64 },
bone: { n:'Кость', icon:'🦴', stack:64 },
gunpowder: { n:'Порох', icon:'💥', stack:64 },
// === FARMING ITEMS ===
wheat: { n:'Пшеница', icon:'🌾', stack:64 },
bread: { n:'Хлеб', icon:'🍞', food:30 },
carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 },
potato: { n:'Картофель', icon:'🥔', stack:64 },
baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 },
wheat_seeds: { n:'Семена пшеницы', icon:'🌱', stack:64 },
mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 },
// === MOB DROP ITEMS ===
scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 },
polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 },
slime_ball: { n:'Слизь', icon:'🟢', stack:64 },
eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 },
// === ARMOR ===
gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 },
};
// Runtime extensions — предметы от обжига
ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' };
ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' };
ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' };
// Axes and shovels
ITEMS.wood_axe = { n:'Деревянный топор', icon:'🪓' };
ITEMS.stone_axe = { n:'Каменный топор', icon:'🪓' };
ITEMS.iron_axe = { n:'Железный топор', icon:'🪓' };
ITEMS.wood_shovel = { n:'Деревянная лопата', icon:'🔱' };
ITEMS.stone_shovel = { n:'Каменная лопата', icon:'🔱' };
ITEMS.iron_shovel = { n:'Железная лопата', icon:'🔱' };

44
src3d/data/recipes.js Normal file
View File

@ -0,0 +1,44 @@
export const RECIPES = [
{ out:'planks', qty:4, cost:{ wood:1 } },
{ out:'ladder', qty:3, cost:{ planks:7 } },
{ out:'torch', qty:2, cost:{ coal:1, planks:1 } },
{ out:'glass', qty:1, cost:{ sand:3 } },
{ out:'brick', qty:1, cost:{ stone:2, clay:1 } },
{ out:'campfire', qty:1, cost:{ wood:1, coal:1 } },
{ out:'tnt', qty:1, cost:{ sand:2, coal:1 } },
{ out:'bed', qty:1, cost:{ wood: 3, planks: 3 } },
{ out:'boat', qty:1, cost:{ wood: 5 } },
{ out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } },
{ out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } },
{ out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } },
{ out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } },
{ out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } },
{ out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } },
{ out:'wood_axe', qty:1, cost:{ wood:3, planks:2 } },
{ out:'stone_axe', qty:1, cost:{ wood:1, stone:3 } },
{ out:'iron_axe', qty:1, cost:{ wood:1, iron_ore:2 } },
{ out:'wood_shovel', qty:1, cost:{ wood:1, planks:1 } },
{ out:'stone_shovel', qty:1, cost:{ wood:1, stone:2 } },
{ out:'iron_shovel', qty:1, cost:{ wood:1, iron_ore:1 } },
{ out:'iron_armor', qty:1, cost:{ iron_ore: 5 } },
{ out:'gold_armor', qty:1, cost:{ gold_ore: 8 } },
{ out:'furnace', qty:1, cost:{ stone: 8 } },
{ out:'bow', qty:1, cost:{ wood: 3, planks: 2 } },
{ out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } },
{ out:'flag', qty:1, cost:{ planks:3, iron_ore:1 } },
{ out:'bread', qty:1, cost:{ wheat: 3 } },
{ out:'farmland', qty:1, cost:{ dirt:1 } },
{ out:'wheat_seeds', qty:1, cost:{ farmland:1, dirt:1 } },
];
// Рецепты печи (обжиг)
export const SMELTING_RECIPES = [
{ in:'sand', qty:1, out:'glass', outQty:1, time:3 },
{ in:'clay', qty:1, out:'brick', outQty:1, time:3 },
{ in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 },
{ in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 },
{ in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 },
{ in:'meat', qty:1, out:'cooked', outQty:1, time:2 },
{ in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 },
{ in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 }
];

66
src3d/data/tools.js Normal file
View File

@ -0,0 +1,66 @@
export const TOOLS = {
wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', damage:1, durability:60, toolType:'pickaxe', toolTier:'wood', speed:2 },
stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', damage:2, durability:132, toolType:'pickaxe', toolTier:'stone', speed:4 },
iron_pickaxe: { n:'Железная кирка', icon:'⛏️', damage:3, durability:250, toolType:'pickaxe', toolTier:'iron', speed:6 },
wood_sword: { n:'Деревянный меч', icon:'🗡️', damage:4, durability:60 },
stone_sword: { n:'Каменный меч', icon:'🗡️', damage:5, durability:132 },
iron_sword: { n:'Железный меч', icon:'🗡️', damage:7, durability:250 },
wood_axe: { n:'Деревянный топор', icon:'🪓', damage:3, durability:60, toolType:'axe', toolTier:'wood', speed:2 },
stone_axe: { n:'Каменный топор', icon:'🪓', damage:4, durability:132, toolType:'axe', toolTier:'stone', speed:4 },
iron_axe: { n:'Железный топор', icon:'🪓', damage:5, durability:250, toolType:'axe', toolTier:'iron', speed:6 },
wood_shovel: { n:'Деревянная лопата', icon:'🔱', damage:1, durability:60, toolType:'shovel', toolTier:'wood', speed:2 },
stone_shovel: { n:'Каменная лопата', icon:'🔱', damage:2, durability:132, toolType:'shovel', toolTier:'stone', speed:4 },
iron_shovel: { n:'Железная лопата', icon:'🔱', damage:3, durability:250, toolType:'shovel', toolTier:'iron', speed:6 },
bow: { n:'Лук', icon:'🏹', damage:10, durability:200 },
};
import { state } from '../core/state.js';
import { BLOCKS } from './blocks.js';
export function addTool(type) {
const maxDur = TOOLS[type]?.durability || 60;
const key = `${type}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`;
state.toolDurability.set(key, { type, current: maxDur, max: maxDur });
}
export function useTool(type) {
for (const [tid, dur] of state.toolDurability) {
if (dur.type === type && dur.current > 0) {
dur.current--;
if (dur.current <= 0) {
state.toolDurability.delete(tid);
state.inv[type] = Math.max(0, (state.inv[type] || 0) - 1);
return true; // broke
}
return false; // used but not broke
}
}
return false; // no durability entry found
}
export function getBestTool(blockType) {
const block = BLOCKS[blockType];
if (!block || !block.toolType) return null;
const toolType = block.toolType;
for (const tier of ['iron', 'stone', 'wood']) {
const toolId = `${tier}_${toolType}`;
if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId;
}
return null;
}
export function toolTierLessThan(toolTier, minTier) {
const tiers = { wood: 1, stone: 2, iron: 3 };
return (tiers[toolTier] || 0) < (tiers[minTier] || 0);
}
export function getMiningSpeed(blockType) {
const block = BLOCKS[blockType];
if (!block) return 1;
const toolId = getBestTool(blockType);
if (toolId) {
const tool = TOOLS[toolId];
if (tool.toolType === block.toolType) return tool.speed;
}
return 1; // Bare hands = 1 click per hit
}

19
src3d/entities/boat.js Normal file
View File

@ -0,0 +1,19 @@
// Лодка
import { state } from '../core/state.js';
export const boat = {
x: 0, y: 0,
w: 34, h: 34,
vx: 0, vy: 0,
active: false,
inWater: false
};
export function initBoat() {
boat.x = 0;
boat.y = 0;
boat.vx = 0;
boat.vy = 0;
boat.active = false;
boat.inWater = false;
}

185
src3d/entities/mob-ai.js Normal file
View File

@ -0,0 +1,185 @@
// Моб AI
import { state } from '../core/state.js';
import { GRAV, GRAV_WATER, TILE } from '../core/constants.js';
import { updateWaterFlag } from '../physics/water-detect.js';
import { calculateDamage } from '../entities/player.js';
import { playSound } from '../audio/sound-engine.js';
import { explodeAt } from '../world/tnt.js';
import { resolveY } from '../physics/collision.js';
import { resolveX } from '../physics/collision.js';
import { isSolid } from '../world/world-storage.js';
import { getCachedBiome } from '../world/generation.js';
export function mobAI(m, dt) {
updateWaterFlag(m);
if (m.kind === 'zombie') {
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
state.player.invuln <= 0) {
const damage = calculateDamage(15);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.vx += dir * 420;
state.player.vy -= 260;
playSound('hit1');
}
} else if (m.kind === 'creeper') {
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (dist < 60) {
m.fuse -= dt;
if (m.fuse <= 0) {
explodeAt(Math.floor((m.x + m.w / 2) / TILE), Math.floor((m.y + m.h / 2) / TILE));
m.hp = 0;
}
} else {
if (dist < 40) {
m.fuse = 0.5;
}
}
} else if (m.kind === 'skeleton') {
const night = isNight();
if (!night) { m.hp -= 30 * dt; m.vx *= 0.5; return; }
const dir = Math.sign((state.player.x) - m.x);
const dist = Math.hypot((state.player.x + state.player.w / 2) - (m.x + m.w / 2), (state.player.y + state.player.h / 2) - (m.y + m.h / 2));
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
m.shootCooldown -= dt;
if (dist < 300 && m.shootCooldown <= 0) {
m.shootCooldown = 2.0;
const dx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2);
const dy = (state.player.y + state.player.h / 2) - (m.y + m.h / 2);
const angle = Math.atan2(dy, dx);
const speed = 450;
state.projectiles.push({
x: m.x + m.w / 2,
y: m.y + m.h / 3,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
dmg: 6,
owner: 'mob',
life: 3
});
}
} else if (m.kind === 'scorpion') {
// Скорпион — бежит к игроку, яд (замедление)
const dir = Math.sign((state.player.x) - m.x);
m.vx = dir * m.speed;
if (m.inWater && Math.random() < 0.06) m.vy = -260;
if (Math.abs((m.x + m.w / 2) - (state.player.x + state.player.w / 2)) < 28 &&
Math.abs((m.y + m.h / 2) - (state.player.y + state.player.h / 2)) < 40 &&
state.player.invuln <= 0) {
const damage = calculateDamage(8);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.slowTimer = 3;
state.player.vx += dir * 300;
state.player.vy -= 200;
playSound('hit1');
}
} else if (m.kind === 'polar_bear') {
// Белый медведь — нейтрален, бродит
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 2.0 + Math.random() * 3;
m.dir = Math.random() < 0.5 ? -1 : 1;
if (Math.random() < 0.3) m.dir = 0;
}
m.vx = m.dir * m.speed;
if (m.inWater) m.vy = -120;
} else if (m.kind === 'slime') {
// Слизь — прыгает к игроку
const dir = Math.sign((state.player.x + state.player.w / 2) - (m.x + m.w / 2));
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 1.5 + Math.random() * 1.5;
m.dir = dir;
m.vy = -200;
}
m.vx = m.dir * m.speed;
} else if (m.kind === 'eagle') {
// Орёл — летает, атакует пикированием
const dx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2);
const dy = (state.player.y + state.player.h / 2) - (m.y + m.h / 2);
const dist = Math.hypot(dx, dy);
if (dist < 400) {
m.vx = Math.sign(dx) * m.speed;
m.vy = dy > 0 ? 60 : -60;
} else {
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 2 + Math.random() * 3;
m.dir = Math.random() < 0.5 ? -1 : 1;
}
m.vx = m.dir * m.speed * 0.5;
m.vy = Math.sin(performance.now() / 1000) * 30;
}
// Атака при касании
if (dist < 30 && state.player.invuln <= 0) {
const damage = calculateDamage(10);
state.player.hp -= damage;
state.player.invuln = 0.8;
state.player.vy -= 200;
playSound('hit1');
}
// Орёл не падает — летающий моб
m.vy *= 0.5;
m.grounded = false;
} else {
// животные (pig, chicken)
m.aiT -= dt;
if (m.aiT <= 0) {
m.aiT = 1.8 + Math.random() * 2.5;
m.dir = Math.random() < 0.5 ? -1 : 1;
if (Math.random() < 0.25) m.dir = 0;
}
m.vx = m.dir * (m.kind === 'chicken' ? 55 : 40);
if (m.inWater) m.vy = -120;
}
// Мобы прыгают при столкновении со стеной
if (m.grounded && Math.abs(m.vx) > 5 && m.kind !== 'eagle') {
const probeX = m.vx > 0 ? m.x + m.w + 2 : m.x - 2;
const probeGY = Math.floor((m.y + m.h - 1) / TILE);
if (isSolid(Math.floor(probeX / TILE), probeGY)) {
m.vy = -280;
}
}
// физика моба
const g = (m.inWater && m.kind !== 'eagle') ? GRAV_WATER : GRAV;
m.vy += g * dt;
m.y += m.vy * dt; m.grounded = false; resolveY(m);
m.x += m.vx * dt; resolveX(m);
}
export function isNight() {
return state.worldTime > 0.5;
}
// Mob loot table
export function getMobLoot(kind) {
const table = {
chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}],
pig: [{item:'meat',min:1,max:2,chance:1}],
zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}],
skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}],
creeper: [{item:'gunpowder',min:1,max:2,chance:1}],
scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}],
polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}],
slime: [{item:'slime_ball',min:1,max:2,chance:1}],
eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}]
};
return table[kind] || [];
}

53
src3d/entities/mobs.js Normal file
View File

@ -0,0 +1,53 @@
// Сущности: животные + зомби + новые мобы
export class Entity {
constructor(x, y, w, h) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.vx = 0;
this.vy = 0;
this.hp = 3;
this.grounded = false;
this.inWater = false;
this.aiT = 0;
this.dir = 1;
}
}
export class Pig extends Entity {
constructor(x, y) { super(x, y, 34, 24); this.kind = 'pig'; this.hp = 2; }
}
export class Chicken extends Entity {
constructor(x, y) { super(x, y, 26, 22); this.kind = 'chicken'; this.hp = 1; }
}
export class Zombie extends Entity {
constructor(x, y) { super(x, y, 34, 50); this.kind = 'zombie'; this.hp = 4; this.speed = 80 + Math.random() * 40; }
}
export class Creeper extends Entity {
constructor(x, y) { super(x, y, 34, 50); this.kind = 'creeper'; this.hp = 4; this.speed = 60 + Math.random() * 30; this.fuse = 3.2; }
}
export class Skeleton extends Entity {
constructor(x, y) { super(x, y, 34, 50); this.kind = 'skeleton'; this.hp = 4; this.speed = 70 + Math.random() * 30; this.shootCooldown = 0; }
}
export class Scorpion extends Entity {
constructor(x, y) { super(x, y, 26, 26); this.kind = 'scorpion'; this.hp = 3; this.speed = 90; this.fuse = 0; this.shootCooldown = 0; }
}
export class PolarBear extends Entity {
constructor(x, y) { super(x, y, 40, 34); this.kind = 'polar_bear'; this.hp = 8; this.speed = 50; this.fuse = 0; this.shootCooldown = 2; }
}
export class Slime extends Entity {
constructor(x, y) { super(x, y, 24, 24); this.kind = 'slime'; this.hp = 2; this.speed = 30; this.fuse = 0; this.shootCooldown = 2; }
}
export class Eagle extends Entity {
constructor(x, y) { super(x, y, 30, 22); this.kind = 'eagle'; this.hp = 3; this.speed = 120; this.fuse = 0; this.shootCooldown = 0; }
}

40
src3d/entities/player.js Normal file
View File

@ -0,0 +1,40 @@
import { TILE } from '../core/constants.js';
import { state } from '../core/state.js';
// Функция для расчёта урона с учётом брони
function calculateDamage(baseDamage) {
const reduction = state.player.armor;
const actualDamage = baseDamage * (1 - reduction);
return actualDamage;
}
// Инициализация игрока
function initPlayer() {
state.player = {
x: 6 * TILE,
y: 0 * TILE,
w: 34,
h: 34,
vx: 0,
vy: 0,
grounded: false,
inWater: false,
headInWater: false,
hp: 100,
hunger: 100,
o2: 100,
invuln: 0,
fallStartY: 0,
lastStepTime: 0,
sleeping: false,
inBoat: false,
armor: 0,
equippedArmor: null,
temperature: 15,
slowTimer: 0
};
state.spawnPoint = { x: 6 * TILE, y: 0 * TILE };
}
export { calculateDamage, initPlayer };

548
src3d/game/loop.js Normal file
View File

@ -0,0 +1,548 @@
// ==================== ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ ====================
import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { playSound } from '../audio/sound-engine.js';
import { getBlock } from '../world/world-storage.js';
import { updateWaterPhysics } from '../world/water.js';
import { updateWaterFlag } from '../physics/water-detect.js';
import { resolveY, resolveX } from '../physics/collision.js';
import { calculateDamage } from '../entities/player.js';
import { isNight } from '../entities/mob-ai.js';
import { tickFurnaces } from '../ui/furnace.js';
import { renderFurnaceUI } from '../ui/furnace.js';
import { updateWeather, updateRain, drawWeather, skyColor } from '../world/weather.js';
import { ensureGenAroundCamera, surfaceGyAt, genColumn, getCachedBiome, getBiome } from '../world/generation.js';
import { Zombie, Creeper, Skeleton, Pig, Chicken, Scorpion, PolarBear, Slime, Eagle } from '../entities/mobs.js';
import { mobAI } from '../entities/mob-ai.js';
import { explodeAt } from '../world/tnt.js';
import { sendPlayerPosition } from '../multiplayer/socket-helpers.js';
import { renderMinimap } from '../ui/minimap.js';
import { rebuildHotbar } from '../ui/hotbar.js';
import { mode } from '../game/modes.js';
import { mouse } from '../input/mouse-handler.js';
import { drawFire } from '../render/draw-fire.js';
import { itemTex } from '../render/textures.js';
import { renderFrame3D } from '../render/render3d.js';
// ==================== LOOP ====================
let last = performance.now();
let prevJump = false;
// При возврате на вкладку — сбрасываем last чтобы не было скачка dt
document.addEventListener('visibilitychange', () => {
if (!document.hidden) last = performance.now();
});
export function loop(now) {
const rawDt = Math.min(0.05, (now - last) / 1000);
last = now;
// Sub-stepping: разбиваем физику на шаги ≤16ms чтобы не проваливаться сквозь блоки
const PHYSICS_STEP = 0.016;
const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP));
const dt = rawDt / steps;
const player = state.player;
const inv = state.inv;
const inp = state.inp;
const clouds = state.clouds;
const mobs = state.mobs;
const projectiles = state.projectiles;
const parts = state.parts;
const boat = state.boat;
const jumpPressed = inp.j && !prevJump;
prevJump = inp.j;
// Ускорение времени во время сна или замороженный цикл
if (!state.dayNightCycle) {
// Цикл день/ночь выключен — время стоит
} else if (player.sleeping && isNight()) {
state.worldTime += dt * 8 / state.DAY_LEN;
player.hp = Math.min(100, player.hp + dt * 20);
if (!isNight()) {
player.sleeping = false;
}
} else {
state.worldTime += dt / state.DAY_LEN;
}
if (state.worldTime >= 1) state.worldTime -= 1;
// камера следует за игроком по X/Y
state.camX = Math.floor((player.x + player.w / 2) - state.W / 2);
state.camY = Math.floor((player.y + player.h / 2) - state.H / 2);
ensureGenAroundCamera();
// clouds parallax
for (const c of clouds) {
c.x -= c.s * dt;
if (c.x + c.w < state.camX - 400) c.x = state.camX + state.W + Math.random() * 700;
}
// player
updateWaterFlag(player);
// кислород/утопление
if (player.headInWater) {
player.o2 = Math.max(0, player.o2 - 6 * dt);
if (player.o2 === 0) {
const damage = calculateDamage(4 * dt);
player.hp -= damage;
}
} else {
player.o2 = Math.min(100, player.o2 + 10 * dt);
}
// голод убывает
player.hunger = Math.max(0, player.hunger - dt * 0.2);
// ========== TEMPERATURE ==========
if (player.temperature === undefined) player.temperature = 15;
const BIOME_TEMP = { snow: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 };
const COLD_THRESHOLD = -5;
const HEAT_THRESHOLD = 35;
const HEAT_RADIUS = 5;
const pGX = Math.floor(player.x / state.TILE);
const biome = getCachedBiome(pGX);
const pGY = Math.floor(player.y / state.TILE);
const sGY = surfaceGyAt(pGX);
const isUndergroundTemp = (pGY - sGY) > 2;
let targetTemp = BIOME_TEMP[biome] || 15;
if (isUndergroundTemp) targetTemp = 15;
if (isNight() && !isUndergroundTemp) targetTemp -= 10;
if ((state.weatherType === 'rain' || state.weatherType === 'storm') && !isUndergroundTemp) targetTemp -= 5;
player.temperature += (targetTemp - player.temperature) * dt * 0.3;
let nearHeat = false, nearCool = false;
for (let dx = -HEAT_RADIUS; dx <= HEAT_RADIUS; dx++) {
for (let dy = -HEAT_RADIUS; dy <= HEAT_RADIUS; dy++) {
const b = getBlock(pGX + dx, pGY + dy);
if (!b || b.dead) continue;
if (b.t === 'campfire' || b.t === 'torch') nearHeat = true;
if (b.t === 'water' && biome === 'desert') nearCool = true;
}
}
if (nearHeat) {
player.temperature += (30 - player.temperature) * dt * 2;
if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt);
}
if (nearCool) {
player.temperature += (15 - player.temperature) * dt * 1.5;
if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) player.hp = Math.min(100, player.hp + 8 * dt);
}
if (isUndergroundTemp && !nearHeat) {
if (player.hp < 100 && player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) {
player.hp = Math.min(100, player.hp + 2 * dt);
}
}
const aboveBlk = getBlock(pGX, pGY - 1);
const inShade = aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass';
if (inShade && !isUndergroundTemp && !nearCool) {
player.temperature -= 5 * dt;
if (player.temperature > HEAT_THRESHOLD - 2 && player.hp < 100) player.hp = Math.min(100, player.hp + 1 * dt);
}
if (player.temperature < COLD_THRESHOLD && !isUndergroundTemp) {
const severity = Math.abs(player.temperature - COLD_THRESHOLD) / 15;
player.hp -= 1 * severity * dt;
if (severity > 0.5) player.vx *= (1 - 0.3 * Math.min(1, severity) * dt);
}
if (player.temperature > HEAT_THRESHOLD && !isUndergroundTemp) {
const severity = (player.temperature - HEAT_THRESHOLD) / 15;
player.hp -= 1 * severity * dt;
if (severity > 0.5) player.hunger -= 0.5 * severity * dt;
}
if (player.slowTimer > 0) player.slowTimer -= dt;
// Кактус — урон при касании
const pCGX = Math.floor((player.x + player.w / 2) / state.TILE);
const pCGY = Math.floor((player.y + player.h / 2) / state.TILE);
for (let cdx = -1; cdx <= 1; cdx++) {
for (let cdy = -1; cdy <= 1; cdy++) {
const cb = getBlock(pCGX + cdx, pCGY + cdy);
if (cb && !cb.dead && BLOCKS[cb.t] && BLOCKS[cb.t].hurt) {
if (player.invuln <= 0) {
player.hp -= 2;
player.invuln = 0.5;
}
}
}
}
// Лёд — скольжение
const iceBlock = getBlock(pCGX, pCGY + 1);
if (iceBlock && BLOCKS[iceBlock.t] && BLOCKS[iceBlock.t].slip && player.grounded) {
player.vx *= 0.98;
}
// Игрок не может двигаться во время сна
if (player.sleeping) {
player.vx = 0;
player.vy = 0;
} else {
const dir = (inp.r ? 1 : 0) - (inp.l ? 1 : 0);
if (dir) player.vx = dir * state.MOVE;
else player.vx *= 0.82;
}
// Звук шагов при движении по земле
if (player.grounded && !player.inWater && Math.abs(player.vx) > 50) {
const stepInterval = 0.35;
if (now / 1000 - player.lastStepTime > stepInterval) {
playSound('step');
player.lastStepTime = now / 1000;
}
}
// прыжок/плавание
if (player.inBoat) {
const dir = (inp.r ? 1 : 0) - (inp.l ? 1 : 0);
if (dir) boat.vx = dir * state.MOVE;
else boat.vx *= 0.95;
boat.vy = 0;
player.x = boat.x + 2;
player.y = boat.y - 4;
player.vx = boat.vx;
player.vy = boat.vy;
player.grounded = true;
player.inWater = false;
if (jumpPressed) {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
player.y += state.TILE;
player.vy = -state.JUMP * 0.5;
playSound('splash');
}
} else if (player.inWater) {
player.vx *= 0.90;
player.vy *= 0.92;
if (!jumpPressed && !inp.j) {
player.vy += state.GRAV_WATER * dt;
} else {
if (jumpPressed) {
player.vy = Math.min(player.vy, -520);
} else if (inp.j) {
player.vy = Math.min(player.vy, -260);
}
}
} else {
if (jumpPressed && player.grounded && !player.sleeping) {
player.vy = -state.JUMP;
player.grounded = false;
player.fallStartY = player.y;
}
}
// Гравитация
if (!player.inWater && !player.inBoat) {
player.vy += state.GRAV * dt;
}
// Обновляем позицию лодки
if (boat.active) {
boat.x += boat.vx * dt;
boat.y += boat.vy * dt;
const boatGX = Math.floor(boat.x / state.TILE);
const boatGY = Math.floor(boat.y / state.TILE);
const below = getBlock(boatGX, boatGY + 1);
if (!below || below.t !== 'water') {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
boat.active = false;
player.y += state.TILE;
player.vy = -200;
playSound('splash');
}
}
if (player.inBoat && !boat.active) {
inv.boat = (inv.boat || 0) + 1;
player.inBoat = false;
player.y += state.TILE;
player.vy = -200;
playSound('splash');
}
// Sub-stepped physics
for (let step = 0; step < steps; step++) {
player.y += player.vy * dt;
resolveY(player);
player.x += player.vx * dt;
resolveX(player);
}
sendPlayerPosition();
updateWaterPhysics(dt);
updateWeather(dt);
updateRain(dt);
player.invuln = Math.max(0, player.invuln - dt);
// Voice position update
state.voicePosT += dt;
if (state.voicePosT > 0.5 && state.voiceSocket && state.voiceSocket.connected) {
state.voicePosT = 0;
state.voiceSocket.emit('voice_pos', { x: player.x, y: player.y });
}
// Furnace tick
tickFurnaces(dt);
if (state.currentFurnaceKey && Math.random() < 0.1) {
renderFurnaceUI();
}
// Projectile tick
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += 400 * dt;
p.life -= dt;
const gx = Math.floor(p.x / state.TILE);
const gy = Math.floor(p.y / state.TILE);
const blk = getBlock(gx, gy);
if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid) {
if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++;
projectiles.splice(i, 1);
continue;
}
if (p.owner === 'mob') {
if (p.x > player.x && p.x < player.x + player.w && p.y > player.y && p.y < player.y + player.h) {
if (player.invuln <= 0) {
player.hp -= calculateDamage(p.dmg);
player.invuln = 0.4;
player.vx += p.vx * 0.3;
player.vy -= 150;
playSound('hit1');
}
projectiles.splice(i, 1);
continue;
}
} else {
let hitMob = false;
if (state.isMultiplayer) {
for (const [id, sm] of state.serverMobs) {
if (sm.dead) continue;
if (p.x > sm.x && p.x < sm.x + sm.w && p.y > sm.y && p.y < sm.y + sm.h) {
state.socket.emit('mob_arrow_hit', { id: sm.id, dmg: p.dmg, vx: p.vx });
projectiles.splice(i, 1);
hitMob = true;
break;
}
}
}
if (!hitMob) {
for (let j = mobs.length - 1; j >= 0; j--) {
const m = mobs[j];
if (p.x > m.x && p.x < m.x + m.w && p.y > m.y && p.y < m.y + m.h) {
m.hp -= p.dmg;
m.vx += p.vx * 0.2;
m.vy -= 200;
if (m.hp <= 0) {
inv.meat += (m.kind === 'chicken' ? 1 : 2);
if (m.kind === 'skeleton') {
inv.arrow += 2 + Math.floor(Math.random() * 3);
if (Math.random() < 0.15) inv.bow = (inv.bow || 0) + 1;
}
mobs.splice(j, 1);
rebuildHotbar();
}
projectiles.splice(i, 1);
break;
}
}
}
}
if (p.life <= 0) projectiles.splice(i, 1);
}
// TNT tick
for (const key of Array.from(state.activeTNT)) {
const b = state.grid.get(key);
if (!b || b.dead) { state.activeTNT.delete(key); continue; }
b.fuse -= dt;
if (b.fuse <= 0) {
explodeAt(b.gx, b.gy);
}
}
// ====== CROP GROWTH ======
for (const [key, blk] of state.grid) {
if (blk.dead) continue;
const bDef = BLOCKS[blk.t];
if (bDef.growthStage === undefined) continue;
if (bDef.harvestable) continue; // stage 3 = fully grown
if (!blk.growthTimer && blk.growthTimer !== 0) blk.growthTimer = 0;
blk.growthTimer += dt;
// Growth time from block def (default 30s per stage)
const growTime = bDef.growthTime || 30;
if (blk.growthTimer >= growTime) {
blk.growthTimer = 0;
const nextStage = bDef.growsTo;
if (nextStage) {
blk.t = nextStage;
// Update texture reference
if (tex[nextStage]) {
// Block type changed — will render new texture next frame
}
}
}
}
// mobs spawn — only in singleplayer
state.spawnT += dt;
if (!state.isMultiplayer && state.spawnT > 1.8 && mobs.length < 30) {
state.spawnT = 0;
const spawnLeft = Math.random() < 0.5;
const gx = spawnLeft
? Math.floor((state.camX - 200) / state.TILE)
: Math.floor((state.camX + state.W + 200) / state.TILE);
genColumn(gx);
const sgy = surfaceGyAt(gx);
const wx = gx * state.TILE + 4;
const wy = (sgy - 2) * state.TILE;
const top = getBlock(gx, sgy);
if (top && top.t === 'water') {
// skip
} else {
const night = isNight();
const spawnBiome = getCachedBiome(gx);
if (night) {
const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton' || m.kind === 'scorpion' || m.kind === 'slime').length;
if (hostileCount < 12) {
const rand = Math.random();
if (spawnBiome === 'desert' && rand < 0.6) {
mobs.push(new Scorpion(wx, wy));
} else if (spawnBiome === 'swamp' && rand < 0.5) {
mobs.push(new Slime(wx, wy));
} else if (rand < 0.35) {
mobs.push(new Zombie(wx, wy));
} else if (rand < 0.55) {
mobs.push(new Creeper(wx, wy));
} else {
mobs.push(new Skeleton(wx, wy));
}
}
}
// Daytime biome mobs
const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken' || m.kind === 'polar_bear' || m.kind === 'eagle').length;
if (animalCount < 8) {
if (spawnBiome === 'tundra' && Math.random() < 0.6) {
mobs.push(new PolarBear(wx, wy));
} else if (spawnBiome === 'mountains' && Math.random() < 0.4) {
mobs.push(new Eagle(wx, wy));
} else {
mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy));
}
}
}
}
// mobs update — только локальные
if (!state.isMultiplayer) {
for (let i = mobs.length - 1; i >= 0; i--) {
const m = mobs[i];
mobAI(m, dt);
if (m.hp <= 0) mobs.splice(i, 1);
}
}
// particles
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
p.t -= dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vy += state.GRAV * dt;
if (p.t <= 0) parts.splice(i, 1);
}
// death
if (player.hp <= 0) {
state.deathEl.style.display = 'flex';
} else if (state.deathEl.style.display === 'flex') {
state.deathEl.style.display = 'none';
}
// ==================== RENDER 3D ====================
const TILE = state.TILE;
const night = isNight();
renderFrame3D(now);
// Стрелы скелета (урон — логика из оригинального рендера, без 2D-отрисовки)
for (const m of mobs) {
if (m.kind === 'skeleton' && m.shootCooldown > 0.5) {
const arrowX = m.x + m.w / 2;
const arrowY = m.y + 15;
const targetX = player.x + player.w / 2;
const targetY = player.y + player.h / 2;
const angle = Math.atan2(targetY - arrowY, targetX - arrowX);
const dx = targetX - arrowX;
const dy = targetY - arrowY;
const dist = Math.hypot(dx, dy);
if (dist < 150 && player.invuln <= 0) {
player.hp -= 8;
player.invuln = 0.5;
player.vx += Math.cos(angle) * 300;
player.vy -= 200;
playSound('hit1');
}
}
}
// HUD — те же DOM-обновления
if (Math.random() < 0.25) {
state.hpEl.textContent = Math.max(0, Math.ceil(player.hp));
state.foodEl.textContent = Math.ceil(player.hunger);
document.getElementById('o2').textContent = Math.ceil(player.o2);
// Temperature display
const tempEl = document.getElementById('temp');
if (tempEl) {
const t = Math.round(player.temperature);
tempEl.textContent = t + '°C';
if (player.temperature < COLD_THRESHOLD) tempEl.style.color = '#74b9ff';
else if (player.temperature > HEAT_THRESHOLD) tempEl.style.color = '#e17055';
else tempEl.style.color = '#55efc4';
}
state.sxEl.textContent = Math.floor(player.x / TILE);
state.syEl.textContent = Math.floor(player.y / TILE);
state.todEl.textContent = night ? 'Ночь' : 'День';
state.worldIdEl.textContent = state.worldId;
if (state.isMultiplayer) {
document.getElementById('multiplayerStatus').style.display = 'flex';
state.playerCountEl.textContent = state.otherPlayers.size + 1;
} else {
document.getElementById('multiplayerStatus').style.display = 'none';
}
}
// Индикатор сна — DOM-оверлей вместо заливки canvas
const sleepOv = document.getElementById('sleepOverlay');
if (sleepOv) sleepOv.style.display = player.sleeping ? 'flex' : 'none';
// Миникарта
if (state.minimapOpen && Math.random() < 0.25) {
renderMinimap();
}
requestAnimationFrame(loop);
}

20
src3d/game/modes.js Normal file
View File

@ -0,0 +1,20 @@
// ==================== РЕЖИМЫ ====================
import { state } from '../core/state.js';
import { playSound } from '../audio/sound-engine.js';
const MODES = [{ id: 'mine', icon: '⛏️' }, { id: 'build', icon: '🧱' }];
export { MODES };
export function mode() {
return MODES[state.modeIdx].id;
}
export function initModes() {
const modeBtn = document.getElementById('modeBtn');
modeBtn.onclick = () => {
playSound('click'); // Звук клика по кнопке режима
state.modeIdx = (state.modeIdx + 1) % MODES.length;
modeBtn.textContent = MODES[state.modeIdx].icon;
};
}

197
src3d/game/save.js Normal file
View File

@ -0,0 +1,197 @@
// ==================== СИСТЕМА СОХРАНЕНИЯ ИГРЫ ====================
import { state } from '../core/state.js';
import { getBlock, setBlock, removeBlock } from '../world/world-storage.js';
import { regenerateVisibleChunks } from '../world/generation.js';
import { rebuildHotbar } from '../ui/hotbar.js';
import { renderInventory } from '../ui/inventory.js';
const SAVE_STORAGE_KEY = state.SAVE_KEY;
let db = null; // Оставляем для совместимости, но не используем
// Инициализация (localStorage + in-memory fallback)
export function initDB() {
return new Promise((resolve) => {
console.log('Используем localStorage для сохранений (sandbox режим)');
resolve(null);
});
}
export function saveGame() {
const saveData = {
version: 2,
worldSeed: state.worldSeed,
player: {
x: state.player.x,
y: state.player.y,
hp: state.player.hp,
hunger: state.player.hunger,
o2: state.player.o2
},
inventory: state.inv,
time: state.worldTime,
isNight: state.isNightTime,
dayNightCycle: state.dayNightCycle,
// Сохраняем только изменения
placedBlocks: state.placedBlocks.slice(),
removedBlocks: state.removedBlocks.slice()
};
const saveSize = JSON.stringify(saveData).length;
console.log('Сохранение: player HP:', state.player.hp, 'hunger:', state.player.hunger, 'o2:', state.player.o2);
// Пробуем сохранить в localStorage (основной метод для персистентности)
try {
localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(saveData));
console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`);
} catch (e) {
console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e);
// Если localStorage недоступен, используем in-memory fallback
state.inMemorySave = saveData;
console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`);
}
}
export function loadGame() {
return new Promise((resolve, reject) => {
// Пробуем localStorage
try {
const localSave = localStorage.getItem(SAVE_STORAGE_KEY);
if (localSave) {
const parsed = JSON.parse(localSave);
console.log('Загружено из localStorage, player HP:', parsed.player?.hp);
resolve(parsed);
return;
}
} catch (e) {
console.warn('Ошибка доступа к localStorage:', e);
}
// Если localStorage недоступен, используем in-memory сохранение
if (state.inMemorySave) {
console.log('Загружено из in-memory сохранения, player HP:', state.inMemorySave.player?.hp);
resolve(state.inMemorySave);
return;
}
console.log('Сохранение не найдено');
resolve(null);
});
}
// Миграция с версии 1 на версию 2
export function migrateV1toV2(saveData) {
console.log('Миграция сохранения с версии 1 на версию 2...');
// Сохраняем seed из текущей игры (так как v1 его не хранил)
saveData.worldSeed = state.worldSeed;
// Инициализируем массивы изменений
saveData.placedBlocks = [];
saveData.removedBlocks = [];
// Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed
// Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed
// и при загрузке просто перегенерируем мир
// Удаляем старые данные
delete saveData.generatedBlocks;
saveData.version = 2;
console.log('Миграция завершена');
}
export async function applySave(saveData) {
if (!saveData) return;
console.log('=== applySave START ===');
console.log('player HP before applySave:', state.player.hp);
console.log('saveData.player.hp:', saveData.player?.hp);
// Миграция версий
if (saveData.version === 1) {
migrateV1toV2(saveData);
}
// Восстанавливаем seed
if (saveData.worldSeed !== undefined) {
state.worldSeed = saveData.worldSeed;
}
// Восстанавливаем игрока
if (saveData.player) {
state.player.x = saveData.player.x;
state.player.y = saveData.player.y;
state.player.hunger = saveData.player.hunger;
state.player.o2 = saveData.player.o2;
// Обновляем spawnPoint на позицию из сохранения
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
// Проверяем HP из сохранения - если <= 0, устанавливаем 100
const savedHP = saveData.player.hp;
console.log('Saved HP from file:', savedHP);
if (savedHP <= 0) {
console.log('WARNING: Saved HP is <= 0, setting to 100!');
state.player.hp = 100;
} else {
state.player.hp = savedHP;
}
console.log('player HP after restore:', state.player.hp);
console.log('spawnPoint обновлён из сохранения: x=', state.spawnPoint.x, 'y=', state.spawnPoint.y);
} else {
console.log('No player data in save, setting default HP: 100');
state.player.hp = 100;
}
console.log('=== applySave END ===');
// Восстанавливаем инвентарь
if (saveData.inventory) {
for (const key in saveData.inventory) {
state.inv[key] = saveData.inventory[key];
}
}
// Восстанавливаем время
if (saveData.time !== undefined) {
state.worldTime = saveData.time;
}
// Восстанавливаем день/ночь
if (saveData.isNight !== undefined) {
state.isNightTime = saveData.isNight;
}
if (saveData.dayNightCycle !== undefined) {
state.dayNightCycle = saveData.dayNightCycle;
}
// Перегенерируем мир по seed
regenerateVisibleChunks();
// Применяем изменения (только для v2)
if (saveData.version === 2) {
// Применяем блоки, установленные игроком
for (const block of saveData.placedBlocks) {
setBlock(block.gx, block.gy, block.t, true);
}
// Применяем удалённые блоки
for (const block of saveData.removedBlocks) {
removeBlock(block.gx, block.gy);
}
// Восстанавливаем массивы изменений
state.placedBlocks = saveData.placedBlocks || [];
state.removedBlocks = saveData.removedBlocks || [];
}
// Fill recentItems from inventory if empty (hotbar won.t render otherwise)
if (state.recentItems.length === 0) {
state.recentItems = Object.keys(state.inv).filter(id => state.inv[id] > 0).slice(0, 5);
}
rebuildHotbar();
renderInventory();
console.log('Игра загружена');
}

37
src3d/input/controls.js vendored Normal file
View File

@ -0,0 +1,37 @@
// ==================== УПРАВЛЕНИЕ ====================
import { state } from '../core/state.js';
export const inp = state.inp;
export function bindHold(el, key) {
const down = (e) => { e.preventDefault(); state.inp[key] = true; };
const up = (e) => { e.preventDefault(); state.inp[key] = false; };
el.addEventListener('pointerdown', down);
el.addEventListener('pointerup', up);
el.addEventListener('pointerleave', up);
}
export function initControls() {
const leftBtn = document.getElementById('left');
const rightBtn = document.getElementById('right');
const jumpBtn = document.getElementById('jump');
const downBtn = document.getElementById('down');
if (leftBtn) bindHold(leftBtn, 'l');
if (rightBtn) bindHold(rightBtn, 'r');
if (jumpBtn) bindHold(jumpBtn, 'j');
if (downBtn) bindHold(downBtn, 's');
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = true;
if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = true;
});
window.addEventListener('keyup', (e) => {
if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = false;
if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = false;
});
}

View File

@ -0,0 +1,420 @@
// ==================== ВЗАИМОДЕЙСТВИЕ МЫШЬ/ТАП ====================
import { state } from '../core/state.js';
import { BLOCKS } from '../data/blocks.js';
import { ITEMS } from '../data/items.js';
import { TOOLS } from '../data/tools.js';
import { getBestTool, useTool, toolTierLessThan, getMiningSpeed } from '../data/tools.js';
import { getBlock, setBlock, removeBlock } from '../world/world-storage.js';
import { playSound } from '../audio/sound-engine.js';
import { rebuildHotbar, addToRecentItems } from '../ui/hotbar.js';
import { mode } from '../game/modes.js';
import { openFurnaceUI } from '../ui/furnace.js';
import { saveGame } from '../game/save.js';
import { activateTNT } from '../world/tnt.js';
import { sendBlockChange } from '../multiplayer/socket-helpers.js';
import { isNight } from '../entities/mob-ai.js';
// Crop stage to seed/harvest mapping
const CROP_SEED_MAP = {
wheat_stage0: 'wheat_seeds', wheat_stage1: 'wheat_seeds', wheat_stage2: 'wheat_seeds', wheat_stage3: 'wheat_seeds',
carrot_stage0: 'carrot', carrot_stage1: 'carrot', carrot_stage2: 'carrot', carrot_stage3: 'carrot',
potato_stage0: 'potato', potato_stage1: 'potato', potato_stage2: 'potato', potato_stage3: 'potato'
};
const CROP_HARVEST_MAP = { wheat_stage3: 'wheat', carrot_stage3: 'carrot', potato_stage3: 'potato' };
// Helper: handle block drops including farming items
function handleBlockDrop(removed, inv) {
// Grass drops wheat_seeds (50%)
if (removed.t === 'grass') {
if (Math.random() < 0.5) inv.wheat_seeds = (inv.wheat_seeds || 0) + 1;
return;
}
// Crop drops: seeds for stages 0-2, harvest+seed for stage 3
const harvestItem = CROP_HARVEST_MAP[removed.t];
const seedItem = CROP_SEED_MAP[removed.t];
if (harvestItem) {
inv[harvestItem] = (inv[harvestItem] || 0) + (BLOCKS[removed.t].harvestQty || 1); addToRecentItems(harvestItem);
if (seedItem) { inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); }
} else if (seedItem) {
// Early crop stage — return seed
inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem);
} else {
inv[removed.t] = (inv[removed.t] || 0) + 1; addToRecentItems(removed.t);
}
}
export const mouse = { x: null, y: null };
// Hold-to-mine tracking
let miningHeld = false;
let miningInterval = null;
export function initMouseHandlers() {
const canvas = state.canvas;
canvas.addEventListener('pointermove', (e) => {
const r = canvas.getBoundingClientRect();
mouse.x = e.clientX - r.left;
mouse.y = e.clientY - r.top;
});
export function handleTap(wx, wy) {
if (state.craftOpen) return;
if (state.player.hp <= 0) return;
const gx = Math.floor(wx / state.TILE);
const gy = Math.floor(wy / state.TILE);
// Пробуждение: клик по любой кровати когда спишь
const b = getBlock(gx, gy);
if (state.player.sleeping && b && b.t === 'bed') {
state.player.sleeping = false;
return;
}
if (state.player.sleeping) return; // Нельзя взаимодействовать во время сна
// Клик по печи — открываем панель обжига
if (b && b.t === 'furnace' && mode() === 'mine') {
openFurnaceUI(gx, gy);
return;
}
// клик по мобу (в режиме mine)
if (mode() === 'mine') {
// Check server mobs first (multiplayer)
if (state.isMultiplayer) {
for (const [id, sm] of state.serverMobs) {
if (sm.dead) continue;
if (wx >= sm.x && wx <= sm.x + sm.w && wy >= sm.y && wy <= sm.y + sm.h) {
let dmg = 1;
const swordTypes = ['iron_sword', 'stone_sword', 'wood_sword'];
for (const st of swordTypes) {
if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; }
}
state.socket.emit('mob_hurt', { id: sm.id, dmg });
playSound('attack');
return;
}
}
}
// Local mobs (singleplayer or if not hit server mob)
for (let i = state.mobs.length - 1; i >= 0; i--) {
const m = state.mobs[i];
if (wx >= m.x && wx <= m.x + m.w && wy >= m.y && wy <= m.y + m.h) {
let dmg = 1;
const swordTypes = ['iron_sword', 'stone_sword', 'wood_sword'];
for (const st of swordTypes) {
if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; }
}
m.hp -= dmg;
m.vx += (m.x - state.player.x) * 2;
m.vy -= 200;
playSound('attack');
if (m.hp <= 0) {
if (m.kind === 'chicken') playSound('hurt_chicken');
state.inv.meat += (m.kind === 'chicken' ? 1 : 2);
if (m.kind === 'skeleton') {
state.inv.arrow += 2 + Math.floor(Math.random() * 3);
if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1;
}
state.mobs.splice(i, 1);
rebuildHotbar();
}
return;
}
}
}
// Лук — стреляем стрелой
if (state.selected === 'bow' && state.inv.bow > 0 && state.inv.arrow > 0) {
const aimX = wx - state.player.x - state.player.w / 2;
const aimY = wy - state.player.y - state.player.h / 2;
const angle = Math.atan2(aimY, aimX);
state.projectiles.push({
x: state.player.x + state.player.w / 2,
y: state.player.y + state.player.h / 3,
vx: Math.cos(angle) * 550,
vy: Math.sin(angle) * 550,
dmg: 10,
owner: 'player',
life: 4
});
state.inv.arrow--;
useTool('bow');
playSound('hit1');
rebuildHotbar();
return;
}
// еда (предмет)
if (ITEMS[state.selected] && state.inv[state.selected] > 0) {
const it = ITEMS[state.selected];
if (state.player.hp < 100 || state.player.hunger < 100) {
playSound('eat1');
state.player.hunger = Math.min(100, state.player.hunger + it.food);
state.player.hp = Math.min(100, state.player.hp + 15);
state.inv[state.selected]--;
rebuildHotbar();
}
return;
}
// жарка на костре: выбран meat + клик по campfire
if (b && b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) {
playSound('fire');
state.inv.meat--;
state.inv.cooked++;
rebuildHotbar();
return;
}
// Сон на кровати: клик по bed
if (b && b.t === 'bed' && isNight()) {
state.player.sleeping = true;
saveGame();
return;
}
// ====== MINING PROGRESSION SYSTEM ======
if (mode() === 'mine') {
if (!b) return;
// Allow mining crops (growthStage) and harvestable blocks, block other decor and fluids
if (BLOCKS[b.t].fluid || (BLOCKS[b.t].decor && BLOCKS[b.t].growthStage === undefined && !BLOCKS[b.t].harvestable)) return;
if (b.t === 'tnt') { activateTNT(b, 3.2); return; }
// Calculate mining progress
const hardness = BLOCKS[b.t].hardness || 1;
if (hardness === Infinity) return; // unbreakable (bedrock)
if (hardness === 0) {
// Instant-mine blocks (flower, boat, crops, etc.)
const removed = removeBlock(gx, gy);
if (removed) {
handleBlockDrop(removed, state.inv);
sendBlockChange(gx, gy, removed.t, 'remove');
rebuildHotbar();
}
return;
}
// Same block being mined?
if (state.mining.gx === gx && state.mining.gy === gy) {
state.mining.progress += getMiningSpeed(b.t);
} else {
// New block, start mining
state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness };
}
// Play mining hit sound
if (b.t === 'glass') playSound('glass1');
else if (b.t === 'sand') playSound('sand1');
else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1');
else if (b.t === 'wood' || b.t === 'planks') playSound('wood1');
else playSound('cloth1');
// Check if block is fully mined
if (state.mining.progress >= hardness) {
// Check drop requirements
const minTool = BLOCKS[b.t].minTool;
let drops = true;
if (minTool) {
const toolId = getBestTool(b.t);
if (toolId) {
const tool = TOOLS[toolId];
if (toolTierLessThan(tool.toolTier, minTool)) drops = false;
else useTool(toolId);
} else {
drops = false;
}
} else {
// Use tool durability if applicable
const toolId = getBestTool(b.t);
if (toolId) useTool(toolId);
}
const removed = removeBlock(gx, gy);
if (removed) {
if (drops) {
handleBlockDrop(removed, state.inv);
} else {
// Drop cobblestone instead for stone-type blocks
if (BLOCKS[removed.t]?.toolType === 'pickaxe') {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
// Else nothing drops
}
sendBlockChange(gx, gy, removed.t, 'remove');
// Sound for block break
if (removed.t === 'glass') playSound('glass1');
else if (removed.t === 'sand') playSound('sand1');
else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1');
else if (removed.t === 'wood') playSound('wood1');
else playSound('cloth1');
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
}
// Start hold-to-mine for desktop
miningHeld = true;
if (miningInterval) clearInterval(miningInterval);
miningInterval = setInterval(() => {
if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; }
// Re-check the block still exists at mining position
const mb = getBlock(state.mining.gx, state.mining.gy);
if (!mb || mb.dead || mb.t !== b.t) {
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
return;
}
// Accumulate progress
if (state.mining.gx !== null && state.mining.hardness > 0) {
state.mining.progress += getMiningSpeed(mb.t) * 0.3; // 0.3x per tick for held mining
if (state.mining.progress >= state.mining.hardness) {
const mgx = state.mining.gx;
const mgy = state.mining.gy;
const mMinTool = BLOCKS[mb.t].minTool;
let mDrops = true;
if (mMinTool) {
const mToolId = getBestTool(mb.t);
if (mToolId) {
const mTool = TOOLS[mToolId];
if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false;
else useTool(mToolId);
} else {
mDrops = false;
}
} else {
const mToolId = getBestTool(mb.t);
if (mToolId) useTool(mToolId);
}
const removed = removeBlock(mgx, mgy);
if (removed) {
if (mDrops) {
handleBlockDrop(removed, state.inv);
} else {
if (BLOCKS[removed.t]?.toolType === 'pickaxe') {
state.inv.cobblestone = (state.inv.cobblestone || 0) + 1;
}
}
sendBlockChange(mgx, mgy, removed.t, 'remove');
rebuildHotbar();
}
state.mining = { gx: null, gy: null, progress: 0, hardness: 0 };
miningHeld = false;
clearInterval(miningInterval);
miningInterval = null;
}
}
}, 100);
return;
}
// ====== BUILD MODE ======
if (mode() === 'build') {
// Planting seeds on farmland (works even without farmland in inventory)
const seedToCropBlock = {
wheat_seeds: 'wheat_stage0',
carrot: 'carrot_stage0',
potato: 'potato_stage0'
};
if (seedToCropBlock[state.selected] && state.inv[state.selected] > 0) {
// Clicking on farmland — plant above it
if (b && b.t === 'farmland') {
const aboveKey = gx + ',' + (gy - 1);
const above = state.grid.get(aboveKey);
if (!above || above.dead) {
setBlock(gx, gy - 1, seedToCropBlock[state.selected], true);
state.inv[state.selected]--;
const cropBlock = state.grid.get(gx + ',' + (gy - 1));
if (cropBlock) cropBlock.growthTimer = 0;
rebuildHotbar();
return;
}
}
// Clicking on empty space above farmland — plant there
if (!b) {
const belowKey = gx + ',' + (gy + 1);
const below = state.grid.get(belowKey);
if (below && below.t === 'farmland') {
setBlock(gx, gy, seedToCropBlock[state.selected], true);
state.inv[state.selected]--;
const cropBlock = state.grid.get(gx + ',' + gy);
if (cropBlock) cropBlock.growthTimer = 0;
rebuildHotbar();
return;
}
}
}
if (state.inv[state.selected] <= 0) return;
if (!BLOCKS[state.selected]) return;
if (b) return; // занято
// Проверяем, ставим ли лодку
if (state.selected === 'boat') {
const waterBelow = getBlock(gx, gy + 1);
if (!waterBelow || waterBelow.t !== 'water') {
return;
}
state.boat.x = gx * state.TILE;
state.boat.y = gy * state.TILE;
state.boat.vx = 0;
state.boat.vy = 0;
state.boat.active = true;
state.boat.inWater = true;
state.player.inBoat = true;
state.player.x = state.boat.x;
state.player.y = state.boat.y;
state.player.vx = 0;
state.player.vy = 0;
playSound('splash');
state.inv[state.selected]--;
rebuildHotbar();
return;
}
// запрет ставить в игрока
const TILE = state.TILE;
const bx = gx * TILE, by = gy * TILE;
const overlap = !(bx >= state.player.x + state.player.w || bx + TILE <= state.player.x || by >= state.player.y + state.player.h || by + TILE <= state.player.y);
if (overlap) return;
setBlock(gx, gy, state.selected, true);
state.inv[state.selected]--;
sendBlockChange(gx, gy, state.selected, 'set');
if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build');
else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build');
else if (state.selected === 'glass') playSound('glass1');
else if (state.selected === 'sand') playSound('sand1');
else if (state.selected === 'dirt' || state.selected === 'grass') playSound('cloth1');
rebuildHotbar();
return;
}
}
export function stopMining() {
miningHeld = false;
if (miningInterval) { clearInterval(miningInterval); miningInterval = null; }
}
// Release mouse — stop hold-to-mine
canvas.addEventListener('pointerup', () => {
stopMining();
});
canvas.addEventListener('pointerleave', () => {
stopMining();
// Reset mining when mouse leaves canvas
});
}

282
src3d/main.js Normal file
View File

@ -0,0 +1,282 @@
// ==================== ENTRY POINT ====================
import { state } from './core/state.js';
import { SERVER_URL } from './config.js';
import { showStartMenu, isTgWebApp, tgUser } from './ui/start-menu.js';
import { initCanvas } from './core/canvas.js';
import { initSocket } from './multiplayer/socket.js';
import { loadSound, playSound } from './audio/sound-engine.js';
import { initTextures, tex } from './render/textures.js';
import { getBlock, setBlock, removeBlock } from './world/world-storage.js';
import { genColumn, surfaceGyAt } from './world/generation.js';
import { initDB, loadGame, applySave, saveGame } from './game/save.js';
import { loop } from './game/loop.js';
import { initChat } from './ui/chat.js';
import { initFurnace } from './ui/furnace.js';
import { initMinimap } from './ui/minimap.js';
import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js';
import { initMarket } from './ui/market.js';
import { initSettings } from './ui/settings.js';
import { initRespawn } from './ui/respawn.js';
import { initShare } from './ui/share.js';
import { initControls } from './input/controls.js';
import { initMouseHandlers } from './input/mouse-handler.js';
import { initModes } from './game/modes.js';
import { initVoice } from './multiplayer/voice-chat.js';
import { rebuildHotbar } from './ui/hotbar.js';
import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js';
// ==================== 1) CONFIG — parse URL ====================
// urlParams defined in config.js
state.SERVER_URL = SERVER_URL;
state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot';
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
// Set TG-related state
if (isTgWebApp) {
state.myTgId = tgUser?.id || null;
}
state.heroImg = new Image(); state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
// ==================== 2) START MENU ====================
// Show the start menu before initializing the game.
// The menu returns a Promise that resolves with the worldId.
async function startGame() {
const worldId = await showStartMenu();
// Set worldId and playerName
state.worldId = worldId;
if (!state.playerName) {
state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок';
localStorage.setItem('minegrechka_playerName', state.playerName);
}
console.log('[Launch] worldId:', state.worldId, 'player:', state.playerName, 'tg:', isTgWebApp);
// Validate TG initData if in TG context
if (isTgWebApp && window.Telegram && Telegram.WebApp.initData) {
try {
const resp = await fetch(state.SERVER_URL + '/api/tg/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ initData: Telegram.WebApp.initData })
});
const data = await resp.json();
if (data.ok && data.tg_id) {
console.log('[TG] Authenticated, tg_id:', data.tg_id);
state.myTgId = data.tg_id;
}
} catch (e) { console.warn('[TG] Auth failed:', e); }
}
// Update URL with world code
try {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('world', state.worldId);
if (typeof history !== 'undefined' && history.replaceState) {
history.replaceState(null, '', newUrl.toString());
}
} catch (e) {}
// ==================== 3) CANVAS — init from canvas module ====================
initCanvas();
// ==================== 4) STATE — set DOM refs ====================
state.hpEl = document.getElementById('hp');
state.foodEl = document.getElementById('food');
state.sxEl = document.getElementById('sx');
state.syEl = document.getElementById('sy');
state.todEl = document.getElementById('tod');
state.worldIdEl = document.getElementById('worldId');
state.playerCountEl = document.getElementById('playerCount');
state.deathEl = document.getElementById('death');
state.hotbarEl = document.getElementById('hotbar');
state.craftPanel = document.getElementById('craftPanel');
state.recipesEl = document.getElementById('recipes');
state.inventoryPanel = document.getElementById('inventoryPanel');
state.inventoryGrid = document.getElementById('inventoryGrid');
state.marketPanel = document.getElementById('marketPanel');
state.marketContent = document.getElementById('marketContent');
state.tradePanel = document.getElementById('tradePanel');
state.tradeContent = document.getElementById('tradeContent');
state.multiplayerStatus = document.getElementById('multiplayerStatus');
// ==================== 5) TEXTURES — initTextures() ====================
initTextures();
state.tex = tex;
// ==================== 6) CRAFT/INVENTORY BUTTONS ====================
const craftBtn = document.getElementById('craftBtn');
const invToggle = document.getElementById('invToggle');
const craftClose = document.getElementById('craftClose');
const inventoryClose = document.getElementById('inventoryClose');
if (craftBtn) craftBtn.onclick = () => toggleCraft();
if (craftClose) craftClose.onclick = () => closeCraft();
if (invToggle) invToggle.onclick = () => toggleInventory();
if (inventoryClose) inventoryClose.onclick = () => closeInventory();
// ==================== 7) CONTROLS — initControls ====================
initControls();
// ==================== 8) MOUSE — initMouseHandlers ====================
initMouseHandlers();
// ==================== 9) UI MODULES — init all UI ====================
initChat();
initFurnace();
initMinimap();
initSaveControls();
initMarket();
initRespawn();
initShare();
initSettings();
initModes();
// ==================== 10) SOCKET — initSocket ====================
initSocket();
// ==================== 11) VOICE — initVoice ====================
initVoice();
// ==================== 12) SAVE — loadGame, applySave ====================
// Ensure hotbar has items on first load
if (state.recentItems.length === 0) {
state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5);
}
// Make sure selected item is valid and in recentItems
if (!state.selected || !state.inv[state.selected] || state.inv[state.selected] <= 0) {
state.selected = state.recentItems[0] || 'dirt';
}
if (!state.recentItems.includes(state.selected)) {
state.recentItems.unshift(state.selected);
state.recentItems = state.recentItems.slice(0, 5);
}
rebuildHotbar();
// Keyboard hotbar switching (1/2/3)
document.addEventListener('keydown', (e) => {
if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return;
if (e.key >= '1' && e.key <= '3') {
const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3);
const idx = parseInt(e.key) - 1;
if (items[idx]) {
state.selected = items[idx];
rebuildHotbar();
}
}
});
// Scroll wheel hotbar cycling
document.addEventListener('wheel', (e) => {
if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return;
const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3);
if (items.length === 0) return;
let idx = items.indexOf(state.selected);
if (idx < 0) idx = 0;
if (e.deltaY > 0) idx = (idx + 1) % items.length;
else idx = (idx - 1 + items.length) % items.length;
state.selected = items[idx];
rebuildHotbar();
e.preventDefault();
}, { passive: false });
initDB().then(async () => {
const loadedSave = await loadGame();
if (loadedSave) {
await applySave(loadedSave);
console.log('Загружено сохранение, HP:', state.player.hp);
if (state.player.hp <= 0) {
console.log('WARNING: HP <= 0 после загрузки, возрождаемся');
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.x = state.spawnPoint.x;
state.player.y = state.spawnPoint.y;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
state.player.fallStartY = state.player.y;
}
} else {
console.log('Сохранение не найдено, начинаем новую игру');
// Fill recentItems from inventory
state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5);
rebuildHotbar();
state.player.hp = 100;
state.player.hunger = 100;
state.player.o2 = 100;
state.player.vx = state.player.vy = 0;
state.player.invuln = 0;
// старт — на поверхности
const startGX = 6;
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
const surfaceY = surfaceGyAt(startGX);
let safeGY = surfaceY - 1;
const aboveBlock = getBlock(startGX, surfaceY - 1);
if (aboveBlock && aboveBlock.t === 'water') {
for (let gy = state.SEA_GY - 1; gy >= 0; gy--) {
const b = getBlock(startGX, gy);
if (!b || b.dead || b.t === 'air' || b.t === 'water') continue;
safeGY = gy - 1;
break;
}
}
state.player.y = safeGY * state.TILE;
state.player.x = startGX * state.TILE;
state.player.fallStartY = state.player.y;
state.spawnPoint.x = state.player.x;
state.spawnPoint.y = state.player.y;
console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y);
// Генерируем карту вокруг стартовой позиции
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
}
// Автосейв при скрытии страницы
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
saveGame();
}
});
// Автосейв перед закрытием страницы
window.addEventListener('beforeunload', () => {
saveGame();
});
}).catch(err => {
console.error('Ошибка инициализации:', err);
const startGX = 6;
genColumn(startGX);
state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE;
state.player.fallStartY = state.player.y;
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
genColumn(gx);
}
});
// ==================== 13) LOOP — startLoop ====================
requestAnimationFrame(loop);
}
// Start the game
startGame().catch(err => {
console.error('Fatal error starting game:', err);
});
// Debug: give items
window.debugGive = function(id, count) {
state.inv[id] = (state.inv[id] || 0) + (count || 1);
if (!state.recentItems.includes(id)) { state.recentItems.unshift(id); state.recentItems = state.recentItems.slice(0, 5); }
rebuildHotbar();
// Can't call renderInventory from here, but user can click inv
console.log('Gave', count || 1, id, '=>', state.inv[id]);
console.log('inv keys>0:', Object.keys(state.inv).filter(k => state.inv[k] > 0).join(', '));
};
window.debugInv = function() { return Object.keys(state.inv).filter(k => state.inv[k] > 0).map(k => k + ':' + state.inv[k]); };

View File

@ -0,0 +1,47 @@
// Socket helpers: цвет, отправка позиции/блоков
import { state } from '../core/state.js';
import { SERVER_URL } from '../config.js';
import { MOVE_SEND_INTERVAL } from '../core/constants.js';
// Генерация случайного цвета для игрока на основе socket_id
export function getRandomPlayerColor(socketId) {
const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4'];
let hash = 0;
for (let i = 0; i < socketId.length; i++) {
hash = ((hash << 5) - hash) + socketId.charCodeAt(i);
hash = hash & hash;
}
return colors[Math.abs(hash) % colors.length];
}
// Throttle для отправки позиции (10-20 раз в секунду)
let lastMoveSendTime = 0;
let lastSentX = 0, lastSentY = 0;
// Отправка позиции игрока (с throttle)
export function sendPlayerPosition() {
if (!state.isMultiplayer || !state.socket || !state.socket.connected) return;
const now = performance.now() / 1000;
if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return;
// Отправляем только если позиция изменилась
const dx = Math.abs(state.player.x - lastSentX);
const dy = Math.abs(state.player.y - lastSentY);
if (dx < 1 && dy < 1) return;
lastMoveSendTime = now;
lastSentX = state.player.x;
lastSentY = state.player.y;
state.socket.emit('player_move', { x: state.player.x, y: state.player.y, player_name: state.playerName });
}
// Отправка изменения блока
export function sendBlockChange(gx, gy, t, op) {
if (!state.isMultiplayer || !state.socket || !state.socket.connected) return;
state.socket.emit('block_change', { gx, gy, t, op });
}

Some files were not shown because too many files have changed in this diff Show More