Compare commits
5 Commits
prod-serve
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
ea0694898e | |
|
|
afb5b8a7c6 | |
|
|
2a5f65d393 | |
|
|
8eebf378ab | |
|
|
81f6a0055a |
|
|
@ -1,4 +0,0 @@
|
|||
backup/
|
||||
backup_v*/
|
||||
*.bak*
|
||||
node_modules/
|
||||
|
|
@ -4,12 +4,6 @@ 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.
|
|
@ -0,0 +1,11 @@
|
|||
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
|
|
@ -0,0 +1,11 @@
|
|||
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
|
|
@ -0,0 +1,27 @@
|
|||
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
|
|
@ -0,0 +1,79 @@
|
|||
<!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> 🍗 <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>
|
||||
|
|
@ -1,28 +1,8 @@
|
|||
/* 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:0; background:#87CEEB; overflow:hidden; }
|
||||
#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:100; display:none; }
|
||||
|
||||
/* Стили для кнопок управления (.btn вместо .cbtn для совместимости с index.html) */
|
||||
.btn { 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; z-index:101; }
|
||||
.btn:active { transform:translateY(-46%); box-shadow:0 3px 0 #111; background:#444; }
|
||||
#left { left:18px; }
|
||||
#right { left:102px; }
|
||||
#down { left:186px; background:#2980b9; border-color:#3498db; }
|
||||
#jump { right:18px; background:#d35400; border-color:#e67e22; }
|
||||
|
||||
/* Скрываем панель на десктопе (широкие экраны) */
|
||||
@media (min-width: 769px) {
|
||||
#controls { display: none !important; }
|
||||
}
|
||||
|
||||
/* Показываем панель на мобильных устройствах (узкие экраны) */
|
||||
@media (max-width: 768px) {
|
||||
#controls { display: block !important; }
|
||||
}
|
||||
#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; }
|
||||
|
|
@ -36,21 +16,14 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; }
|
|||
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; }
|
||||
#mapToggle { top:10px; right:70px !important; background:#1abc9c; }
|
||||
#saveBtn { top:10px; right:70px !important; background:#27ae60; }
|
||||
#resetBtn { top:10px; right:130px !important; background:#e74c3c; }
|
||||
#craftBtn { top:74px; right:10px !important; background:#9b59b6; }
|
||||
#invToggle { top:74px; right:70px !important; background:#3498db; }
|
||||
#menuBtn { top:74px; right:130px !important; background:#555; }
|
||||
#chatToggle { display: none !important; }
|
||||
#chatPanel { display: none !important; }
|
||||
#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%; }
|
||||
|
||||
/* Поднимаем hotbar на тач-устройствах, чтобы не перекрывать кнопки */
|
||||
body.touch-device #hotbar {
|
||||
bottom: 150px;
|
||||
}
|
||||
.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; }
|
||||
|
|
@ -60,12 +33,12 @@ body.touch-device #hotbar {
|
|||
/* 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; z-index: 200; }
|
||||
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: 200; }
|
||||
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;
|
||||
|
|
@ -73,11 +46,9 @@ body.touch-device #hotbar {
|
|||
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; }
|
||||
.equipped-indicator { position: absolute; top: 2px; right: 2px; font-size: 14px; color: #2ecc71; font-weight: 900; text-shadow: 1px 1px 0 #000; }
|
||||
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; }
|
||||
#inventoryPanel .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; }
|
||||
|
|
@ -87,30 +58,15 @@ body.touch-device #hotbar {
|
|||
.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; }
|
||||
|
||||
/* Panel header + close button fix */
|
||||
.panel-header { display:flex; justify-content:space-between; align-items:center; color:#fff; font-weight:900; font-size:18px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid rgba(255,255,255,0.15); }
|
||||
.panel-header .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 12px; border-radius:10px; cursor:pointer; font-size:16px; min-width:36px; text-align:center; flex-shrink:0; margin-left:12px; }
|
||||
|
||||
/* Custom modal (alerts/confirms) */
|
||||
.custom-modal-overlay { position:absolute; inset:0; background:rgba(0,0,0,0.7); z-index:9999; display:flex; align-items:center; justify-content:center; }
|
||||
.custom-modal-box { background:#1a1a2e; border:2px solid #e74c3c; border-radius:16px; padding:24px 32px; color:#fff; font-size:16px; font-weight:700; text-align:center; max-width:320px; box-shadow:0 8px 32px rgba(0,0,0,0.5); }
|
||||
.custom-modal-box .modal-btns { display:flex; gap:10px; justify-content:center; margin-top:16px; }
|
||||
.custom-modal-box button { font-weight:900; padding:10px 20px; border-radius:10px; font-size:15px; cursor:pointer; border:none; color:#fff; }
|
||||
.custom-modal-box .btn-yes { background:#e74c3c; }
|
||||
.custom-modal-box .btn-no { background:#555; }
|
||||
.custom-modal-box .btn-ok { background:#2ecc71; }
|
||||
|
||||
/* Menu dropdown ⋯ */
|
||||
.menu-dropdown { position:absolute; top:120px; right:58px; background:rgba(20,20,30,0.95); border:2px solid rgba(255,255,255,0.7); border-radius:10px; display:none; flex-direction:column; min-width:140px; z-index:300; pointer-events:auto; overflow:hidden; }
|
||||
.menu-dropdown.open { display:flex; }
|
||||
.menu-item { padding:10px 14px; color:#fff; font-size:14px; font-weight:700; cursor:pointer; border-bottom:1px solid rgba(255,255,255,0.1); }
|
||||
.menu-item:last-child { border-bottom:none; }
|
||||
.menu-item:hover { background:rgba(255,255,255,0.12); }
|
||||
.menu-item:active { background:rgba(255,255,255,0.2); }
|
||||
|
||||
/* Market panel */
|
||||
#marketPanel { display:none; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:360px; max-width:90%; background:rgba(10,10,12,0.95); border:2px solid rgba(255,255,255,0.85); border-radius:14px; pointer-events:auto; padding:12px; z-index:200; }
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
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
|
|
@ -0,0 +1,79 @@
|
|||
<!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> 🍗 <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>
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/* 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; }
|
||||
33
build.js
33
build.js
|
|
@ -2,19 +2,20 @@
|
|||
/**
|
||||
* Simple ES-module → IIFE bundler for GrechkaCraft
|
||||
* Resolves import/export, wraps in single IIFE, no external deps.
|
||||
* Usage: node build.js > game.js
|
||||
* Usage: node bundle.js > ../game-bundled.js
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SRC = path.join(__dirname, 'src');
|
||||
const ENTRY = 'main.js';
|
||||
const ENTRY = process.argv[2] || 'main.js';
|
||||
|
||||
const visited = new Set();
|
||||
const chunks = [];
|
||||
|
||||
const fileExports = {};
|
||||
const fileImports = {};
|
||||
// Named export → variable name mapping per file
|
||||
const fileExports = {}; // file -> [{local, exported}]
|
||||
const fileImports = {}; // file -> [{names, from}]
|
||||
|
||||
function resolveImportPath(fromPath, importerDir) {
|
||||
let resolved = path.resolve(importerDir, fromPath);
|
||||
|
|
@ -26,6 +27,7 @@ function resolveImportPath(fromPath, importerDir) {
|
|||
|
||||
function collectExports(content) {
|
||||
const exports = [];
|
||||
// export { a, b as c }
|
||||
const re1 = /export\s*\{([^}]+)\}/g;
|
||||
let m;
|
||||
while ((m = re1.exec(content)) !== null) {
|
||||
|
|
@ -35,11 +37,15 @@ function collectExports(content) {
|
|||
});
|
||||
exports.push(...names);
|
||||
}
|
||||
const re2 = /export\s+(?:async\s+)?(?:const|let|var|function|class)\s+(\w+)/g;
|
||||
// export const/let/var/function/class name
|
||||
const re2 = /export\s+(?:const|let|var|function|class)\s+(\w+)/g;
|
||||
while ((m = re2.exec(content)) !== null) {
|
||||
exports.push({ local: m[1], exported: m[1] });
|
||||
}
|
||||
// export default ...
|
||||
if (/export\s+default\s+/.test(content)) {
|
||||
// Find the expression after export default
|
||||
// Simpler: just use __default as name
|
||||
exports.push({ local: '__default__', exported: 'default' });
|
||||
}
|
||||
return exports;
|
||||
|
|
@ -56,6 +62,7 @@ function collectImports(content) {
|
|||
});
|
||||
imports.push({ names, from: m[2] });
|
||||
}
|
||||
// import default
|
||||
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] });
|
||||
|
|
@ -71,9 +78,11 @@ function processFile(filePath) {
|
|||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const dir = path.dirname(filePath);
|
||||
|
||||
// Collect exports/imports BEFORE stripping
|
||||
fileExports[relPath] = collectExports(content);
|
||||
fileImports[relPath] = collectImports(content);
|
||||
|
||||
// Process dependencies first (depth-first)
|
||||
for (const imp of fileImports[relPath]) {
|
||||
const depPath = resolveImportPath(imp.from, dir);
|
||||
processFile(depPath);
|
||||
|
|
@ -84,14 +93,26 @@ function processFile(filePath) {
|
|||
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+(const|let|var|function|class)\s/g, '$1 ');
|
||||
content = content.replace(/export\s*\{[^}]*\};?/g, '');
|
||||
|
||||
chunks.push({ relPath, content });
|
||||
}
|
||||
|
||||
// Phase 1: Collect all files, build dependency graph
|
||||
processFile(path.join(SRC, ENTRY));
|
||||
|
||||
// Phase 2: Build rename map
|
||||
// For each file's import { X as Y } from './other.js', we need to know:
|
||||
// What is X called in other.js? Then rename Y → X_original
|
||||
// Build: originalName (in source file) → what it's called elsewhere
|
||||
|
||||
// Simpler approach: since we wrap everything in one scope,
|
||||
// we just need to ensure no name collisions.
|
||||
// For now: trust that most names are unique across modules.
|
||||
// If collision, prefix with module path.
|
||||
|
||||
// Phase 3: Emit
|
||||
let output = '// GrechkaCraft — auto-bundled from ES modules\n';
|
||||
output += '(function() {\n';
|
||||
output += '"use strict";\n\n';
|
||||
|
|
|
|||
107
build3d.js
107
build3d.js
|
|
@ -1,107 +0,0 @@
|
|||
#!/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);
|
||||
5865
game.js.bak.v30
5865
game.js.bak.v30
File diff suppressed because it is too large
Load Diff
5190
game.js_new
5190
game.js_new
File diff suppressed because it is too large
Load Diff
230
index-v2.html
230
index-v2.html
|
|
@ -1,230 +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, 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> 🍗 <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>
|
||||
121
index.html
121
index.html
|
|
@ -2,61 +2,13 @@
|
|||
<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: УБРАН (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">
|
||||
<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">
|
||||
</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>
|
||||
|
|
@ -65,7 +17,6 @@
|
|||
<div id="stats">
|
||||
<div class="row">❤️ <span id="hp">100</span> 🍗 <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>
|
||||
|
|
@ -86,14 +37,12 @@
|
|||
<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>
|
||||
|
||||
<!-- Печь -->
|
||||
|
|
@ -161,31 +110,45 @@
|
|||
<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 id="settingsContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
|
||||
<script src="game.js?v=79"></script>
|
||||
<script src="game.js?v=44"></script>
|
||||
<!-- Выбор игры: Grechka Craft / Grechka Armageddon -->
|
||||
<style>
|
||||
#gamePick{position:fixed;inset:0;z-index:2147483647;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.6);font-family:sans-serif}
|
||||
#gamePick .box{background:#fff;border-radius:12px;padding:24px 32px;text-align:center;max-width:86vw}
|
||||
#gamePick h1{font-size:22px;margin:0 0 16px;color:#111}
|
||||
#gamePick .pick{display:block;width:270px;margin:10px auto;padding:12px 24px;font-size:18px;border:none;border-radius:10px;cursor:pointer;color:#fff}
|
||||
#gamePick .craft{background:#27ae60}#gamePick .arma{background:#2980b9}
|
||||
|
||||
</style>
|
||||
<div id="gamePick"><div class="box">
|
||||
<h1>Во что играем?</h1>
|
||||
<button class="pick craft" id="pickCraft">⛏️ Grechka Craft</button>
|
||||
<button class="pick arma" id="pickArma">💥 Grechka Armageddon</button>
|
||||
</div></div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
try {
|
||||
var q = new URLSearchParams(location.search).get('play');
|
||||
var ov = document.getElementById('gamePick');
|
||||
// выбор — всегда вручную, никаких автопереходов
|
||||
if (q === 'craft') { try { localStorage.setItem('grechka-game-choice','craft'); } catch(e){} return; }
|
||||
if (q === 'armageddon') { location.replace('/worms/'); return; }
|
||||
ov.style.display = 'flex';
|
||||
document.getElementById('pickCraft').onclick = function(){
|
||||
try { localStorage.setItem('grechka-game-choice','craft'); } catch(e){}
|
||||
ov.style.display = 'none';
|
||||
};
|
||||
document.getElementById('pickArma').onclick = function(){
|
||||
try { localStorage.setItem('grechka-game-choice','armageddon'); } catch(e){}
|
||||
location.href = '/worms/';
|
||||
};
|
||||
} catch(e){}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,110 +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?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> 🍗 <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
125
index.html_new
|
|
@ -1,125 +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, 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> 🍗 <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>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
nginx.conf
18
nginx.conf
|
|
@ -3,17 +3,9 @@ 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";
|
||||
|
|
@ -22,14 +14,6 @@ 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 +0,0 @@
|
|||
../esbuild/bin/esbuild
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# esbuild
|
||||
|
||||
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
|
||||
Binary file not shown.
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# 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.
|
||||
Binary file not shown.
|
|
@ -1,300 +0,0 @@
|
|||
"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);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,716 +0,0 @@
|
|||
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 {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,469 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"esbuild": "^0.28.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ====================
|
||||
// This module only sets SERVER_URL. Player name and world ID are handled in main.js
|
||||
// to avoid duplicate prompts.
|
||||
|
||||
// Возможность переопределить сервер через query string
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
export const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru';
|
||||
|
||||
|
|
@ -10,3 +8,60 @@ 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 };
|
||||
|
|
@ -1,41 +1,27 @@
|
|||
// ==================== ХОЛСТ И РАЗМЕРЫ ====================
|
||||
import { state } from './state.js';
|
||||
|
||||
export function initCanvas() {
|
||||
const gameEl = document.getElementById('game');
|
||||
const canvas = document.getElementById('c');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const gameEl = document.getElementById('game');
|
||||
const canvas = document.getElementById('c');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// offscreen light map
|
||||
const lightC = document.createElement('canvas');
|
||||
const lightCtx = lightC.getContext('2d');
|
||||
// offscreen light map (не вставляем в DOM)
|
||||
const lightC = document.createElement('canvas');
|
||||
const lightCtx = lightC.getContext('2d');
|
||||
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
let W = 0, H = 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 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);
|
||||
}
|
||||
|
||||
// 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; }
|
||||
function setW(v) { W = v; }
|
||||
function setH(v) { H = v; }
|
||||
|
||||
export { canvas, ctx, lightC, lightCtx, dpr, W, H, gameEl, resize, setW, setH };
|
||||
|
|
@ -1,80 +1,36 @@
|
|||
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';
|
||||
import { TILE } 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",
|
||||
selected: 0,
|
||||
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,
|
||||
|
|
@ -88,33 +44,6 @@ 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,
|
||||
|
|
@ -135,9 +64,7 @@ export const state = {
|
|||
sleeping: false,
|
||||
inBoat: false,
|
||||
armor: 0,
|
||||
equippedArmor: null,
|
||||
temperature: 15,
|
||||
slowTimer: 0
|
||||
equippedArmor: null
|
||||
},
|
||||
|
||||
// Точка спавна
|
||||
|
|
@ -153,21 +80,10 @@ 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,
|
||||
wood_axe: 0, stone_axe: 0, iron_axe: 0,
|
||||
wood_shovel: 0, stone_shovel: 0, iron_shovel: 0,
|
||||
iron_armor: 0, gold_armor: 0,
|
||||
iron_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
|
||||
iron_ingot: 0, gold_ingot: 0, copper_ingot: 0
|
||||
},
|
||||
|
||||
// Лодка
|
||||
|
|
@ -180,7 +96,7 @@ export const state = {
|
|||
},
|
||||
|
||||
// Ввод
|
||||
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 },
|
||||
inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false },
|
||||
|
||||
// Мышь
|
||||
mouse: { x: 0, y: 0 },
|
||||
|
|
@ -226,25 +142,16 @@ export const state = {
|
|||
|
||||
// Прочность инструментов
|
||||
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,
|
||||
};
|
||||
heroImg: null
|
||||
};
|
||||
|
|
@ -1,55 +1,30 @@
|
|||
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 },
|
||||
};
|
||||
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 }
|
||||
};
|
||||
|
|
@ -2,36 +2,9 @@ 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:'🔱' };
|
||||
ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' };
|
||||
|
|
@ -14,31 +14,19 @@ 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:'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 } },
|
||||
{ out:'arrow', qty:4, cost:{ stone: 1, wood: 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 }
|
||||
];
|
||||
{ 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 } // булыжник → камень
|
||||
];
|
||||
|
|
@ -1,21 +1,14 @@
|
|||
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 },
|
||||
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 } }
|
||||
};
|
||||
|
||||
import { state } from '../core/state.js';
|
||||
import { BLOCKS } from './blocks.js';
|
||||
|
||||
export function addTool(type) {
|
||||
const maxDur = TOOLS[type]?.durability || 60;
|
||||
|
|
@ -36,31 +29,4 @@ 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
|
||||
}
|
||||
|
|
@ -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,15 +27,20 @@ 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) {
|
||||
|
|
@ -43,17 +48,23 @@ 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;
|
||||
|
|
@ -71,72 +82,8 @@ 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;
|
||||
|
|
@ -147,17 +94,8 @@ 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 && m.kind !== 'eagle') ? GRAV_WATER : GRAV;
|
||||
const g = m.inWater ? GRAV_WATER : GRAV;
|
||||
m.vy += g * dt;
|
||||
|
||||
m.y += m.vy * dt; m.grounded = false; resolveY(m);
|
||||
|
|
@ -165,21 +103,6 @@ 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] || [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
// Сущности: животные + зомби + новые мобы
|
||||
// Сущности: животные + зомби
|
||||
|
||||
export class Entity {
|
||||
constructor(x, y, w, h) {
|
||||
|
|
@ -34,20 +34,4 @@ 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,12 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -28,13 +32,12 @@ function initPlayer() {
|
|||
lastStepTime: 0,
|
||||
sleeping: false,
|
||||
inBoat: false,
|
||||
armor: 0,
|
||||
equippedArmor: null,
|
||||
temperature: 15,
|
||||
slowTimer: 0
|
||||
armor: 0, // Текущий уровень защиты (0 = без брони, 0.5 = железная броня)
|
||||
equippedArmor: null // Тип надетой брони
|
||||
};
|
||||
|
||||
// Сохраняем начальную позицию для возрождения
|
||||
state.spawnPoint = { x: 6 * TILE, y: 0 * TILE };
|
||||
}
|
||||
|
||||
export { calculateDamage, initPlayer };
|
||||
export { calculateDamage, initPlayer };
|
||||
376
src/game/loop.js
376
src/game/loop.js
|
|
@ -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 '../world/water.js';
|
||||
import { updateWaterPhysics } from '../physics/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 { 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 { mobAI } from '../entities/mob-ai.js';
|
||||
import { explodeAt } from '../world/tnt.js';
|
||||
import { sendPlayerPosition } from '../multiplayer/socket-helpers.js';
|
||||
|
|
@ -21,7 +21,6 @@ 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();
|
||||
|
|
@ -53,12 +52,12 @@ export function loop(now) {
|
|||
const jumpPressed = inp.j && !prevJump;
|
||||
prevJump = inp.j;
|
||||
|
||||
// Ускорение времени во время сна или замороженный цикл
|
||||
if (!state.dayNightCycle) {
|
||||
// Цикл день/ночь выключен — время стоит
|
||||
} else if (player.sleeping && isNight()) {
|
||||
state.worldTime += dt * 8 / state.DAY_LEN;
|
||||
// Ускорение времени во время сна
|
||||
if (player.sleeping && isNight()) {
|
||||
state.worldTime += dt * 8 / state.DAY_LEN; // В 8 раз быстрее
|
||||
// Восстанавливаем здоровье во время сна
|
||||
player.hp = Math.min(100, player.hp + dt * 20);
|
||||
// Автоматическое пробуждение когда наступает день
|
||||
if (!isNight()) {
|
||||
player.sleeping = false;
|
||||
}
|
||||
|
|
@ -71,19 +70,6 @@ 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
|
||||
|
|
@ -95,96 +81,19 @@ export function loop(now) {
|
|||
// player
|
||||
updateWaterFlag(player);
|
||||
|
||||
// кислород/утопление
|
||||
// кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем
|
||||
if (player.headInWater) {
|
||||
player.o2 = Math.max(0, player.o2 - 6 * dt);
|
||||
player.o2 = Math.max(0, player.o2 - 6 * dt); // замедлил в 3.7 раза
|
||||
if (player.o2 === 0) {
|
||||
const damage = calculateDamage(4 * dt);
|
||||
player.hp -= damage;
|
||||
}
|
||||
} else {
|
||||
player.o2 = Math.min(100, player.o2 + 10 * dt);
|
||||
player.o2 = Math.min(100, player.o2 + 10 * dt); // замедлил восстановление в 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;
|
||||
}
|
||||
// голод убывает, но HP не отнимает (как просили)
|
||||
player.hunger = Math.max(0, player.hunger - dt * 0.2); // замедлил в 4 раза
|
||||
|
||||
// Игрок не может двигаться во время сна
|
||||
if (player.sleeping) {
|
||||
|
|
@ -198,50 +107,64 @@ 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;
|
||||
|
|
@ -249,7 +172,7 @@ export function loop(now) {
|
|||
}
|
||||
}
|
||||
|
||||
// Гравитация
|
||||
// Гравитация применяется только вне воды и вне лодки
|
||||
if (!player.inWater && !player.inBoat) {
|
||||
player.vy += state.GRAV * dt;
|
||||
}
|
||||
|
|
@ -259,11 +182,13 @@ 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;
|
||||
|
|
@ -273,6 +198,7 @@ export function loop(now) {
|
|||
}
|
||||
}
|
||||
|
||||
// Проверяем, не доплыл ли игрок из лодки
|
||||
if (player.inBoat && !boat.active) {
|
||||
inv.boat = (inv.boat || 0) + 1;
|
||||
player.inBoat = false;
|
||||
|
|
@ -281,7 +207,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);
|
||||
|
|
@ -289,10 +215,13 @@ export function loop(now) {
|
|||
resolveX(player);
|
||||
}
|
||||
|
||||
// Отправляем позицию на сервер (мультиплеер)
|
||||
sendPlayerPosition();
|
||||
|
||||
// Обновляем физику воды
|
||||
updateWaterPhysics(dt);
|
||||
|
||||
// Погода и дождь
|
||||
updateWeather(dt);
|
||||
updateRain(dt);
|
||||
|
||||
|
|
@ -308,28 +237,33 @@ 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++;
|
||||
// Врезался в стену
|
||||
if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++; // подбираем ~50%
|
||||
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);
|
||||
|
|
@ -342,6 +276,7 @@ export function loop(now) {
|
|||
continue;
|
||||
}
|
||||
} else {
|
||||
// Попал в моба — server mobs first in multiplayer
|
||||
let hitMob = false;
|
||||
if (state.isMultiplayer) {
|
||||
for (const [id, sm] of state.serverMobs) {
|
||||
|
|
@ -355,6 +290,7 @@ 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) {
|
||||
|
|
@ -377,6 +313,7 @@ export function loop(now) {
|
|||
}
|
||||
}
|
||||
|
||||
// Таймаут
|
||||
if (p.life <= 0) projectiles.splice(i, 1);
|
||||
}
|
||||
|
||||
|
|
@ -390,35 +327,12 @@ export function loop(now) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// ====== 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
|
||||
// mobs spawn (с обеих сторон камеры) — только в одиночном режиме
|
||||
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)
|
||||
|
|
@ -429,21 +343,18 @@ 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) {
|
||||
const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton' || m.kind === 'scorpion' || m.kind === 'slime').length;
|
||||
// Ночью спавним враждебных мобов (максимум 12 хостайл)
|
||||
const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton').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) {
|
||||
if (rand < 0.35) {
|
||||
mobs.push(new Zombie(wx, wy));
|
||||
} else if (rand < 0.55) {
|
||||
mobs.push(new Creeper(wx, wy));
|
||||
|
|
@ -452,21 +363,15 @@ export function loop(now) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Daytime biome mobs
|
||||
const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken' || m.kind === 'polar_bear' || m.kind === 'eagle').length;
|
||||
// Животные спавнятся и днём и ночью (с лимитом)
|
||||
const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken').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.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mobs update — только локальные
|
||||
// mobs update — только локальные (singleplayer)
|
||||
if (!state.isMultiplayer) {
|
||||
for (let i = mobs.length - 1; i >= 0; i--) {
|
||||
const m = mobs[i];
|
||||
|
|
@ -489,6 +394,7 @@ 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';
|
||||
}
|
||||
|
||||
|
|
@ -503,10 +409,10 @@ export function loop(now) {
|
|||
const night = isNight();
|
||||
|
||||
// sky
|
||||
const playerGx = Math.floor(player.x / state.TILE); const skyBiome = getBiome(playerGx); ctx.fillStyle = skyColor(night, skyBiome, state.weatherType, state.worldTime);
|
||||
ctx.fillStyle = night ? '#070816' : (state.isRaining ? '#6B7B8D' : '#87CEEB');
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// clouds
|
||||
// clouds (parallax x/y)
|
||||
ctx.save();
|
||||
ctx.translate(-camX * 0.5, -camY * 0.15);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.65)';
|
||||
|
|
@ -526,7 +432,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;
|
||||
|
|
@ -541,7 +447,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);
|
||||
|
|
@ -557,42 +463,6 @@ 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) {
|
||||
|
|
@ -610,6 +480,7 @@ 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';
|
||||
|
|
@ -617,16 +488,22 @@ 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);
|
||||
|
|
@ -639,6 +516,7 @@ 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';
|
||||
|
|
@ -653,76 +531,17 @@ 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
|
||||
// other players (multiplayer)
|
||||
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;
|
||||
|
|
@ -735,14 +554,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();
|
||||
|
|
@ -810,7 +629,7 @@ export function loop(now) {
|
|||
|
||||
ctx.restore();
|
||||
|
||||
// lighting overlay at night
|
||||
// lighting overlay at night (Minecraft-style: offscreen lightmap, wall occlusion, warm flicker)
|
||||
if (night) {
|
||||
const lightC = state.lightC;
|
||||
const lightCtx = state.lightCtx;
|
||||
|
|
@ -824,16 +643,16 @@ export function loop(now) {
|
|||
lightCtx.globalCompositeOperation = 'destination-out';
|
||||
|
||||
function castLight(sx, sy, radius) {
|
||||
const flick = 0.97 + Math.sin(now / 300 + sx * 0.01) * 0.015 + Math.sin(now / 470 + sy * 0.02) * 0.015;
|
||||
const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06;
|
||||
const r = radius * flick;
|
||||
const steps2 = 24;
|
||||
const steps2 = 12;
|
||||
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.3; step < r; step += TILE * 0.35) {
|
||||
for (let step = TILE * 0.5; step < r; step += TILE * 0.6) {
|
||||
const lgx = Math.floor((sx + ddx * step) / TILE);
|
||||
const lgy = Math.floor((sy + ddy * step) / TILE);
|
||||
const blk = getBlock(lgx, lgy);
|
||||
|
|
@ -883,13 +702,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.9 + Math.sin(now / 350 + b.gx * 3.7) * 0.05 + Math.sin(now / 530 + b.gy * 2.3) * 0.05;
|
||||
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 wx = b.gx * TILE + TILE / 2 - camX;
|
||||
const wy = b.gy * TILE + TILE / 2 - camY;
|
||||
const r = def.lightRadius * 0.85;
|
||||
const r = def.lightRadius * 0.6 * flick;
|
||||
const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r);
|
||||
grad.addColorStop(0, `rgba(255,180,80,0.28)`);
|
||||
grad.addColorStop(0.5, `rgba(255,140,40,0.13)`);
|
||||
grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`);
|
||||
grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`);
|
||||
grad.addColorStop(1, 'rgba(255,100,20,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
|
|
@ -900,21 +719,12 @@ export function loop(now) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
// Дождь
|
||||
drawWeather(ctx);
|
||||
// Дождь (после ночного оверлея)
|
||||
drawRain();
|
||||
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 ? 'Ночь' : 'День';
|
||||
|
|
@ -939,7 +749,7 @@ export function loop(now) {
|
|||
ctx.fillText('Нажмите на кровать чтобы проснуться', W / 2, H / 2 + 40);
|
||||
}
|
||||
|
||||
// Миникарта
|
||||
// Миникарта (обновляем раз в ~4 кадра для оптимизации)
|
||||
if (state.minimapOpen && Math.random() < 0.25) {
|
||||
renderMinimap();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ 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;
|
||||
const SAVE_KEY = state.SAVE_KEY;
|
||||
let db = null; // Оставляем для совместимости, но не используем
|
||||
|
||||
// Инициализация (localStorage + in-memory fallback)
|
||||
|
|
@ -30,7 +29,6 @@ export function saveGame() {
|
|||
inventory: state.inv,
|
||||
time: state.worldTime,
|
||||
isNight: state.isNightTime,
|
||||
dayNightCycle: state.dayNightCycle,
|
||||
// Сохраняем только изменения
|
||||
placedBlocks: state.placedBlocks.slice(),
|
||||
removedBlocks: state.removedBlocks.slice()
|
||||
|
|
@ -41,7 +39,7 @@ export function saveGame() {
|
|||
|
||||
// Пробуем сохранить в localStorage (основной метод для персистентности)
|
||||
try {
|
||||
localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(saveData));
|
||||
localStorage.setItem(SAVE_KEY, JSON.stringify(saveData));
|
||||
console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`);
|
||||
} catch (e) {
|
||||
console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e);
|
||||
|
|
@ -56,7 +54,7 @@ export function loadGame() {
|
|||
return new Promise((resolve, reject) => {
|
||||
// Пробуем localStorage
|
||||
try {
|
||||
const localSave = localStorage.getItem(SAVE_STORAGE_KEY);
|
||||
const localSave = localStorage.getItem(SAVE_KEY);
|
||||
if (localSave) {
|
||||
const parsed = JSON.parse(localSave);
|
||||
console.log('Загружено из localStorage, player HP:', parsed.player?.hp);
|
||||
|
|
@ -163,9 +161,6 @@ export async function applySave(saveData) {
|
|||
if (saveData.isNight !== undefined) {
|
||||
state.isNightTime = saveData.isNight;
|
||||
}
|
||||
if (saveData.dayNightCycle !== undefined) {
|
||||
state.dayNightCycle = saveData.dayNightCycle;
|
||||
}
|
||||
|
||||
// Перегенерируем мир по seed
|
||||
regenerateVisibleChunks();
|
||||
|
|
@ -187,11 +182,6 @@ 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('Игра загружена');
|
||||
}
|
||||
|
|
@ -3,52 +3,19 @@ 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 { rebuildHotbar } 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;
|
||||
|
||||
|
|
@ -158,7 +125,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]--;
|
||||
|
|
@ -169,7 +136,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();
|
||||
|
|
@ -179,209 +146,74 @@ 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;
|
||||
// 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 (BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return;
|
||||
|
||||
if (b.t === 'tnt') { activateTNT(b, 3.2); 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;
|
||||
}
|
||||
const removed = removeBlock(gx, gy);
|
||||
if (removed) {
|
||||
state.inv[removed.t] = (state.inv[removed.t] || 0) + 1;
|
||||
|
||||
// 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;
|
||||
// Тратим прочность кирки (если есть в инвентаре)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Отправляем изменение блока на сервер
|
||||
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');
|
||||
|
||||
rebuildHotbar();
|
||||
}
|
||||
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();
|
||||
|
|
@ -394,29 +226,22 @@ 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);
|
||||
setBlock(gx, gy, state.selected, true); // 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
|
||||
});
|
||||
}
|
||||
140
src/main.js
140
src/main.js
|
|
@ -1,11 +1,11 @@
|
|||
// ==================== 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 { 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 { initTextures, tex, itemTex } 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';
|
||||
|
|
@ -15,7 +15,6 @@ 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';
|
||||
|
|
@ -26,7 +25,7 @@ import { rebuildHotbar } from './ui/hotbar.js';
|
|||
import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js';
|
||||
|
||||
// ==================== 1) CONFIG — parse URL ====================
|
||||
// urlParams defined in config.js
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
state.SERVER_URL = SERVER_URL;
|
||||
state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot';
|
||||
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
|
||||
|
|
@ -36,7 +35,7 @@ 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';
|
||||
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.
|
||||
|
|
@ -103,6 +102,7 @@ async function startGame() {
|
|||
// ==================== 5) TEXTURES — initTextures() ====================
|
||||
initTextures();
|
||||
state.tex = tex;
|
||||
state.itemTex = itemTex;
|
||||
|
||||
// ==================== 6) CRAFT/INVENTORY BUTTONS ====================
|
||||
const craftBtn = document.getElementById('craftBtn');
|
||||
|
|
@ -129,7 +129,6 @@ async function startGame() {
|
|||
initMarket();
|
||||
initRespawn();
|
||||
initShare();
|
||||
initSettings();
|
||||
initModes();
|
||||
|
||||
// ==================== 10) SOCKET — initSocket ====================
|
||||
|
|
@ -139,47 +138,8 @@ async function startGame() {
|
|||
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) {
|
||||
|
|
@ -199,9 +159,6 @@ async function startGame() {
|
|||
}
|
||||
} 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;
|
||||
|
|
@ -263,96 +220,9 @@ async function startGame() {
|
|||
|
||||
// ==================== 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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]); };
|
||||
|
|
|
|||
|
|
@ -1,299 +0,0 @@
|
|||
// ==================== 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);
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
// 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) {
|
||||
|
|
@ -17,7 +15,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)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Socket.IO клиент — инициализация и все обработчики
|
||||
import { state } from '../core/state.js';
|
||||
import { SERVER_URL } from '../config.js';
|
||||
import { SERVER_URL, worldId, playerName } from '../config.js';
|
||||
import { TILE, SEA_GY } from '../core/constants.js';
|
||||
import { BLOCKS } from '../data/blocks.js';
|
||||
import { k, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage.js';
|
||||
import { k, setBlock, removeBlock, grid, blocks } 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,25 +12,6 @@ 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;
|
||||
|
||||
|
|
@ -43,23 +24,17 @@ 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: ${state.worldId}`);
|
||||
console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`);
|
||||
state.mySocketId = socket.id;
|
||||
state.isMultiplayer = true;
|
||||
|
||||
// Присоединяемся к миру
|
||||
socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName });
|
||||
socket.emit('join_world', { world_id: worldId, player_name: playerName });
|
||||
|
||||
// Показываем в UI
|
||||
if (state.worldIdEl) state.worldIdEl.textContent = state.worldId;
|
||||
if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block';
|
||||
|
||||
// Initialize trade socket handlers
|
||||
initTradeSocket(socket);
|
||||
state.worldIdEl.textContent = worldId;
|
||||
state.multiplayerStatus.style.display = 'block';
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
|
|
@ -71,18 +46,20 @@ export function initSocket() {
|
|||
console.log('Disconnected from server');
|
||||
state.isMultiplayer = false;
|
||||
state.otherPlayers.clear();
|
||||
if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none';
|
||||
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;
|
||||
|
|
@ -91,10 +68,12 @@ 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') {
|
||||
|
|
@ -103,19 +82,25 @@ export function initSocket() {
|
|||
}
|
||||
}
|
||||
|
||||
// Устанавливаем время
|
||||
if (data.time !== undefined) {
|
||||
state.worldTime = data.time;
|
||||
state.isNightTime = state.worldTime > 0.5;
|
||||
}
|
||||
|
||||
// Spawn point
|
||||
// Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt
|
||||
// Это гарантирует совпадение с terrain generation
|
||||
{
|
||||
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;
|
||||
|
|
@ -128,6 +113,7 @@ 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;
|
||||
|
|
@ -135,12 +121,14 @@ 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) {
|
||||
|
|
@ -153,25 +141,24 @@ 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) {
|
||||
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);
|
||||
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 };
|
||||
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);
|
||||
|
|
@ -185,32 +172,37 @@ 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 ===
|
||||
// === MOB SYNC (multiplayer) ===
|
||||
|
||||
socket.on('mob_spawned', (data) => {
|
||||
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);
|
||||
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 };
|
||||
state.serverMobs.set(data.id, sm);
|
||||
});
|
||||
|
||||
|
|
@ -226,6 +218,7 @@ 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') {
|
||||
|
|
@ -254,6 +247,7 @@ 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 });
|
||||
|
|
@ -264,11 +258,13 @@ 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;
|
||||
|
|
@ -280,4 +276,4 @@ export function initSocket() {
|
|||
console.error('Error initializing socket:', e);
|
||||
state.isMultiplayer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,46 @@
|
|||
// Голосовой чат
|
||||
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;
|
||||
let voiceMode = 'near'; // 'near' — 600px radius, 'world' — global
|
||||
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: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;';
|
||||
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;';
|
||||
document.querySelector('.ui').appendChild(voiceBtn);
|
||||
|
||||
// Кнопка режима (рядом / весь мир)
|
||||
const voiceModeBtn = document.createElement('div');
|
||||
voiceModeBtn.innerHTML = '📢';
|
||||
voiceModeBtn.title = 'Режим: рядом (600px)';
|
||||
voiceModeBtn.style.cssText = 'position:absolute;top:74px;right:190px;width:48px;height:52px;border-radius:12px;background:#3498db;z-index:200;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;border:2px solid rgba(255,255,255,0.7);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;color:#fff;font-weight:bold;';
|
||||
document.querySelector('.ui').appendChild(voiceModeBtn);
|
||||
|
||||
voiceModeBtn.onclick = () => {
|
||||
if (voiceMode === 'near') {
|
||||
voiceMode = 'world';
|
||||
voiceModeBtn.innerHTML = '🌍';
|
||||
voiceModeBtn.title = 'Режим: весь мир';
|
||||
voiceModeBtn.style.background = '#e67e22';
|
||||
} else {
|
||||
voiceMode = 'near';
|
||||
voiceModeBtn.innerHTML = '📢';
|
||||
voiceModeBtn.title = 'Режим: рядом (600px)';
|
||||
voiceModeBtn.style.background = '#3498db';
|
||||
}
|
||||
if (voiceSocket && voiceSocket.connected) {
|
||||
voiceSocket.emit('voice_mode', { mode: voiceMode });
|
||||
}
|
||||
};
|
||||
|
||||
// Индикатор говорящего
|
||||
const speakingIndicator = document.createElement('div');
|
||||
speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;';
|
||||
|
|
@ -65,20 +89,19 @@ voiceBtn.onclick = async () => {
|
|||
// Подключаемся к голосовому серверу
|
||||
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
|
||||
voiceSocket.on('connect', () => {
|
||||
voiceSocket.emit('voice_join', { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' });
|
||||
voiceSocket.emit('voice_join', { world_id: worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок', mode: voiceMode });
|
||||
});
|
||||
|
||||
voiceSocket.on('voice_in', (payload) => {
|
||||
// Воспроизводим входящий голос
|
||||
const { data, meta, volume: rawVolume } = payload;
|
||||
const volume = rawVolume !== undefined ? rawVolume : 1.0;
|
||||
const { data, meta, volume } = payload;
|
||||
if (!audioCtx || audioCtx.state === 'closed') return;
|
||||
|
||||
// Int16 → Float32 (volume applied via gain node, not in conversion)
|
||||
// Int16 → Float32
|
||||
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);
|
||||
float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF) * volume;
|
||||
}
|
||||
|
||||
const buf = audioCtx.createBuffer(1, float32.length, 24000);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,7 @@ 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' || 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');
|
||||
return !!(b && b.t === 'water');
|
||||
}
|
||||
|
||||
function updateWaterFlag(e) {
|
||||
|
|
@ -26,9 +19,6 @@ 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);
|
||||
|
||||
|
|
@ -36,11 +26,6 @@ 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, isSwampWaterAt, updateWaterFlag };
|
||||
export { isWaterAt, updateWaterFlag };
|
||||
|
|
@ -17,10 +17,10 @@ export function renderLighting(ctx, W, H, camX, camY, lightC, lightCtx, dpr, blo
|
|||
|
||||
// Функция: рисуем мягкий луч света с затуханием за стенами
|
||||
function castLight(sx, sy, radius) {
|
||||
const flick = 0.97 + Math.sin(now / 300 + sx * 0.01) * 0.015 + Math.sin(now / 470 + sy * 0.02) * 0.015;
|
||||
const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06;
|
||||
const r = radius * flick;
|
||||
// 12 лучей — достаточно для мягкого круга
|
||||
const steps = 24;
|
||||
const steps = 12;
|
||||
// Собираем дистанции до стен по лучам
|
||||
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.3; step < r; step += TILE * 0.35) {
|
||||
for (let step = TILE * 0.5; step < r; step += TILE * 0.6) {
|
||||
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.9 + Math.sin(now / 350 + b.gx * 3.7) * 0.05 + Math.sin(now / 530 + b.gy * 2.3) * 0.05;
|
||||
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 wx = b.gx * TILE + TILE / 2 - camX;
|
||||
const wy = b.gy * TILE + TILE / 2 - camY;
|
||||
const r = def.lightRadius * 0.85;
|
||||
const r = def.lightRadius * 0.6 * flick;
|
||||
const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r);
|
||||
grad.addColorStop(0, `rgba(255,180,80,0.28)`);
|
||||
grad.addColorStop(0.5, `rgba(255,140,40,0.13)`);
|
||||
grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`);
|
||||
grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`);
|
||||
grad.addColorStop(1, 'rgba(255,100,20,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.beginPath();
|
||||
|
|
|
|||
|
|
@ -3,313 +3,6 @@ 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');
|
||||
|
|
@ -345,12 +38,16 @@ 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;
|
||||
|
|
@ -361,20 +58,26 @@ 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);
|
||||
|
|
@ -382,96 +85,6 @@ 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);
|
||||
|
|
@ -488,19 +101,4 @@ 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); });
|
||||
}
|
||||
}
|
||||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -5,15 +5,19 @@ 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, itemTex } from '../render/textures.js';
|
||||
import { tex } 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) {
|
||||
if ((inv[res] || 0) < r.cost[res]) return false;
|
||||
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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -27,19 +31,17 @@ 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';
|
||||
|
|
@ -68,10 +70,8 @@ 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,33 +82,35 @@ 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';
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,4 @@ 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 marketPanel = document.getElementById('marketPanel');
|
||||
export const marketContent = document.getElementById('marketContent');
|
||||
export const tradePanel = document.getElementById('tradePanel');
|
||||
export const tradeContent = document.getElementById('tradeContent');
|
||||
export const inventoryGrid = document.getElementById('inventoryGrid');
|
||||
184
src/ui/hotbar.js
184
src/ui/hotbar.js
|
|
@ -1,151 +1,81 @@
|
|||
// Hotbar UI — 3-slot Minecraft-style bottom bar + selected item indicator
|
||||
// Hotbar 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 { tex, itemTex } from '../render/textures.js';
|
||||
import { tex } 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 = '';
|
||||
|
||||
// Get up to 3 recent items with count > 0
|
||||
const items = state.recentItems.filter(id => inv[id] > 0).slice(0, HOTBAR_SLOTS);
|
||||
// Показываем последние 5 выбранных предметов (если они есть в инвентаре)
|
||||
const items = recentItems.filter(id => inv[id] > 0).slice(0, 5);
|
||||
|
||||
// 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;
|
||||
for (const id of items) {
|
||||
const s = document.createElement('div');
|
||||
s.className = 'slot' + (id && id === selected ? ' sel' : '');
|
||||
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();
|
||||
};
|
||||
|
||||
// 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);
|
||||
// Показываем индикатор надетой брони
|
||||
if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') {
|
||||
const equipped = document.createElement('div');
|
||||
equipped.className = 'equipped-indicator';
|
||||
equipped.textContent = '✓';
|
||||
s.appendChild(equipped);
|
||||
}
|
||||
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
@ -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, itemTex } from '../render/textures.js';
|
||||
import { tex } from '../render/textures.js';
|
||||
import { playSound } from '../audio/sound-engine.js';
|
||||
import { rebuildHotbar } from './hotbar.js';
|
||||
|
||||
|
|
@ -13,35 +13,26 @@ 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];
|
||||
// Canvas sprite: block > itemTex > emoji fallback > unknown item fallback
|
||||
if (BLOCKS[id] && tex[id]) {
|
||||
if (BLOCKS[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');
|
||||
|
|
@ -50,11 +41,12 @@ 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);
|
||||
// Обновляем список последних предметов
|
||||
state.recentItems = state.recentItems.filter(item => item !== id); // Удаляем если уже есть
|
||||
state.recentItems.unshift(id); // Добавляем в начало
|
||||
state.recentItems = state.recentItems.slice(0, 5); // Оставляем только 5
|
||||
rebuildHotbar();
|
||||
renderInventory();
|
||||
};
|
||||
|
|
@ -62,12 +54,16 @@ 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();
|
||||
|
|
@ -77,4 +73,4 @@ export function renderInventory() {
|
|||
|
||||
inventoryGrid.appendChild(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,21 +2,19 @@
|
|||
import { state } from '../core/state.js';
|
||||
import { playSound } from '../audio/sound-engine.js';
|
||||
|
||||
let marketPanel, marketClose, marketContent;
|
||||
const marketPanel = document.getElementById('marketPanel');
|
||||
const marketClose = document.getElementById('marketClose');
|
||||
const marketContent = document.getElementById('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());
|
||||
// Кнопка закрытия
|
||||
marketClose.addEventListener('click', () => closeMarket());
|
||||
|
||||
// Слушаем событие от save-controls.js
|
||||
document.addEventListener('toggleMarket', () => toggleMarket());
|
||||
}
|
||||
|
||||
export function toggleMarket() {
|
||||
if (!marketPanel) return;
|
||||
if (marketPanel.style.display === 'none' || !marketPanel.style.display) {
|
||||
openMarket();
|
||||
} else {
|
||||
|
|
@ -25,33 +23,49 @@ export function toggleMarket() {
|
|||
}
|
||||
|
||||
export function closeMarket() {
|
||||
if (marketPanel) marketPanel.style.display = 'none';
|
||||
marketPanel.style.display = 'none';
|
||||
}
|
||||
|
||||
function openMarket() {
|
||||
if (marketPanel) {
|
||||
marketPanel.style.display = 'block';
|
||||
renderMarket();
|
||||
}
|
||||
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>';
|
||||
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('');
|
||||
|
||||
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> ×${o.offer_qty}
|
||||
<span style="color:#888;"> ⇄ </span>
|
||||
<span style="color:#7ecfff;">${o.want_item}</span> ×${o.want_qty}
|
||||
</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);
|
||||
|
|
@ -59,26 +73,42 @@ function renderMarket() {
|
|||
}
|
||||
|
||||
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('');
|
||||
const invItems = Object.keys(state.inv).filter(id => state.inv[id] > 0);
|
||||
|
||||
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>';
|
||||
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;">
|
||||
${invItems.map(id => `<option value="${id}">${id} (×${state.inv[id]})</option>`).join('')}
|
||||
<option value="">— Пусто —</option>
|
||||
</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; }
|
||||
document.getElementById('cancelOrder').addEventListener('click', () => renderMarket());
|
||||
document.getElementById('submitOrder').addEventListener('click', () => {
|
||||
const offerItem = document.getElementById('offerItem').value;
|
||||
const wantItem = document.getElementById('wantItem').value.trim();
|
||||
if (!offerItem || !wantItem) {
|
||||
alert('Выберите предмет и укажите что хотите получить');
|
||||
return;
|
||||
}
|
||||
if (state.socket) {
|
||||
state.socket.emit('market_create', { offer_item: offer, offer_qty: 1, want_item: want, want_qty: 1 });
|
||||
state.socket.emit('market_create', {
|
||||
offer_item: offerItem,
|
||||
offer_qty: 1,
|
||||
want_item: wantItem,
|
||||
want_qty: 1
|
||||
});
|
||||
playSound('click');
|
||||
setTimeout(renderMarket, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -26,15 +26,6 @@ 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() {
|
||||
|
|
@ -119,4 +110,4 @@ export function renderMinimap() {
|
|||
minimapCtx.fillRect(dx * scale, dy * scale, 2, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
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;
|
||||
|
||||
|
|
@ -13,144 +11,99 @@ export function initSaveControls() {
|
|||
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');
|
||||
|
||||
// Убить возможный старый onclick
|
||||
if (menuBtn) menuBtn.onclick = null;
|
||||
// Убить возможный старый onclick (предотвращает race condition)
|
||||
menuBtn.onclick = null;
|
||||
|
||||
// Тогл меню
|
||||
if (menuBtn) {
|
||||
menuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownOpen = !dropdownOpen;
|
||||
if (menuDropdown) menuDropdown.classList.toggle('open', dropdownOpen);
|
||||
});
|
||||
}
|
||||
menuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdownOpen = !dropdownOpen;
|
||||
menuDropdown.classList.toggle('open', dropdownOpen);
|
||||
});
|
||||
|
||||
// Закрытие по клику вне
|
||||
document.addEventListener('click', () => {
|
||||
if (dropdownOpen) {
|
||||
dropdownOpen = false;
|
||||
if (menuDropdown) menuDropdown.classList.remove('open');
|
||||
menuDropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
if (menuDropdown) {
|
||||
menuDropdown.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
// Предотвращаем закрытие при клике внутри дропдауна
|
||||
menuDropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// 💾 Сохранить
|
||||
if (saveItem) {
|
||||
saveItem.addEventListener('click', () => {
|
||||
playSound('click');
|
||||
saveGame();
|
||||
dropdownOpen = false;
|
||||
if (menuDropdown) menuDropdown.classList.remove('open');
|
||||
alert('Игра сохранена!');
|
||||
});
|
||||
}
|
||||
saveItem.addEventListener('click', () => {
|
||||
playSound('click');
|
||||
saveGame();
|
||||
dropdownOpen = false;
|
||||
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');
|
||||
resetItem.addEventListener('click', () => {
|
||||
if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) {
|
||||
playSound('click');
|
||||
|
||||
try {
|
||||
localStorage.removeItem(state.SAVE_KEY);
|
||||
console.log('Сохранение удалено из localStorage');
|
||||
} catch (e) {
|
||||
console.warn('Ошибка удаления сохранения:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.inMemorySave = null;
|
||||
state.worldId = Math.random().toString(36).substring(2, 10);
|
||||
console.log('Новый worldId после сброса:', state.worldId);
|
||||
|
||||
try {
|
||||
const newUrl = new URL(window.location.href);
|
||||
newUrl.searchParams.set('world', state.worldId);
|
||||
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
|
||||
window.history.replaceState(null, '', newUrl.toString());
|
||||
console.log('URL обновлён:', newUrl.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка обновления URL:', e);
|
||||
}
|
||||
|
||||
location.reload();
|
||||
} else {
|
||||
dropdownOpen = false;
|
||||
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();
|
||||
});
|
||||
}
|
||||
marketItem.addEventListener('click', () => {
|
||||
dropdownOpen = false;
|
||||
menuDropdown.classList.remove('open');
|
||||
// toggleMarket импортируется в main.js, вызовем через кастомное событие
|
||||
document.dispatchEvent(new CustomEvent('toggleMarket'));
|
||||
});
|
||||
|
||||
// ⚙️ Настройки
|
||||
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(); }
|
||||
document.dispatchEvent(new CustomEvent('toggleSettings'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Показываем/скрываем пункт сохранения в зависимости от режима
|
||||
export function updateSaveButtonVisibility() {
|
||||
const saveItem = document.getElementById('saveItem');
|
||||
if (!saveItem) return;
|
||||
saveItem.style.display = (state.isMultiplayer && state.otherPlayers.size > 0) ? 'none' : 'block';
|
||||
}
|
||||
if (state.isMultiplayer && state.otherPlayers.size > 0) {
|
||||
saveItem.style.display = 'none';
|
||||
} else {
|
||||
saveItem.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
// Настройки
|
||||
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;
|
||||
}
|
||||
124
src/ui/share.js
124
src/ui/share.js
|
|
@ -1,108 +1,38 @@
|
|||
// ==================== ПОДЕЛИТЬСЯ МИРОМ ====================
|
||||
import { state } from '../core/state.js';
|
||||
import { isTgWebApp } from './start-menu.js';
|
||||
import { showToast } from './start-menu.js';
|
||||
|
||||
export function initShare() {
|
||||
// Keep worldId click-to-copy as before
|
||||
const worldIdEl = document.getElementById('worldId');
|
||||
if (worldIdEl) {
|
||||
worldIdEl.onclick = () => shareWorld();
|
||||
}
|
||||
// Обработчик клика на 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function shareWorld() {
|
||||
const shareUrl = new URL(window.location.href);
|
||||
shareUrl.searchParams.set('world', state.worldId);
|
||||
const url = shareUrl.toString();
|
||||
const shareUrlString = shareUrl.toString();
|
||||
|
||||
// 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;
|
||||
// Копируем в буфер обмена
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(shareUrlString).then(() => {
|
||||
alert('Ссылка скопирована!');
|
||||
}).catch(() => {
|
||||
alert('Ссылка на мир:\n' + shareUrlString);
|
||||
});
|
||||
} else {
|
||||
alert('Ссылка на мир:\n' + shareUrlString);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -78,13 +78,13 @@ const mcCSS = `
|
|||
.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; }
|
||||
padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000; }
|
||||
.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-body { padding:16px;background:#C6C6C6; }
|
||||
.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;
|
||||
|
|
@ -125,11 +125,6 @@ const mcCSS = `
|
|||
.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);
|
||||
|
|
@ -138,12 +133,8 @@ export function showStartMenu() {
|
|||
? 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) {
|
||||
// If world is in URL or TG start param — skip menu, auto-join
|
||||
if ((worldParam && worldParam.trim() !== '') || tgStartParam) {
|
||||
const worldId = (worldParam && worldParam.trim()) || tgStartParam;
|
||||
console.log('[Menu] Joining world from URL/startParam:', worldId);
|
||||
|
||||
|
|
@ -157,7 +148,6 @@ export function showStartMenu() {
|
|||
localStorage.setItem('minegrechka_playerName', state.playerName);
|
||||
}
|
||||
|
||||
hideBoot();
|
||||
resolve(worldId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -512,8 +502,8 @@ export function showStartMenu() {
|
|||
body.appendChild(btnFriends);
|
||||
}
|
||||
|
||||
// Player name input — always visible, with Play button
|
||||
{
|
||||
// Player name input (only if not already set)
|
||||
if (!state.playerName) {
|
||||
const nameDivider = document.createElement('hr');
|
||||
nameDivider.className = 'mc-divider';
|
||||
body.appendChild(nameDivider);
|
||||
|
|
@ -521,53 +511,28 @@ export function showStartMenu() {
|
|||
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.placeholder = 'Ваше имя';
|
||||
nameInput.value = 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();
|
||||
nameInput.addEventListener('input', () => {
|
||||
if (nameInput.value.trim()) {
|
||||
state.playerName = nameInput.value.trim();
|
||||
localStorage.setItem('minegrechka_playerName', state.playerName);
|
||||
}
|
||||
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);
|
||||
body.appendChild(nameInput);
|
||||
}
|
||||
|
||||
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') || 'Игрок';
|
||||
// Pre-fill player name from TG if available
|
||||
if (isTgWebApp && tgUser && !state.playerName) {
|
||||
state.playerName = tgUser.username || tgUser.first_name || 'Игрок';
|
||||
localStorage.setItem('minegrechka_playerName', state.playerName);
|
||||
}
|
||||
|
||||
|
|
|
|||
277
src/ui/trade.js
277
src/ui/trade.js
|
|
@ -1,277 +0,0 @@
|
|||
// 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}...`);
|
||||
}
|
||||
|
|
@ -5,94 +5,16 @@ 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) {
|
||||
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);
|
||||
}
|
||||
// базовая поверхность выше уровня воды с вариациями + "горы"
|
||||
// Используем 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 - тем выше
|
||||
return h;
|
||||
}
|
||||
|
||||
|
|
@ -101,50 +23,37 @@ 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++) {
|
||||
const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water';
|
||||
setBlock(gx, gy, blockType);
|
||||
setBlock(gx, gy, 'water');
|
||||
}
|
||||
// пляж
|
||||
setBlock(gx, sgy, 'sand');
|
||||
} else {
|
||||
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');
|
||||
}
|
||||
// верхний блок: снег на высоких точках
|
||||
if (sgy < SEA_GY - 10) setBlock(gx, sgy, 'stone');
|
||||
else setBlock(gx, sgy, 'grass');
|
||||
}
|
||||
|
||||
// === Подповерхностные слои ===
|
||||
// подповерхностные слои
|
||||
for (let gy = sgy + 1; gy <= BEDROCK_GY; gy++) {
|
||||
if (gy === BEDROCK_GY) {
|
||||
setBlock(gx, gy, 'bedrock');
|
||||
continue;
|
||||
}
|
||||
|
||||
let t = BIOMES[biome].subsurface;
|
||||
let t = 'stone';
|
||||
|
||||
// глубже — камень
|
||||
if (gy > sgy + 3) t = 'stone';
|
||||
// ближе к поверхности
|
||||
if (gy <= sgy + 3) t = 'dirt';
|
||||
|
||||
// пустыня: sand глубже
|
||||
if (biome === 'desert' && gy <= sgy + 6) t = 'sand';
|
||||
// биомы/материалы
|
||||
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';
|
||||
|
||||
// болото: глина ближе к поверхности
|
||||
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') {
|
||||
|
|
@ -158,69 +67,20 @@ function genColumn(gx) {
|
|||
setBlock(gx, gy, t);
|
||||
}
|
||||
|
||||
// === Растительность и декор ===
|
||||
const b = BIOMES[biome];
|
||||
// Деревья и цветы (только на траве, и не в воде)
|
||||
const top = getBlock(gx, sgy);
|
||||
|
||||
// цветы (только 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 (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');
|
||||
}
|
||||
}
|
||||
|
||||
// кактусы (пустыня)
|
||||
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);
|
||||
}
|
||||
|
||||
// Применяем серверные оверрайды для этой колонны
|
||||
|
|
@ -245,6 +105,7 @@ 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);
|
||||
}
|
||||
|
|
@ -262,4 +123,4 @@ function seededRandom(gx, gy) {
|
|||
return n - Math.floor(n);
|
||||
}
|
||||
|
||||
export { generated, surfaceGyAt, genColumn, regenerateVisibleChunks, ensureGenAroundCamera, seededRandom, PYRAMID_PATTERN, HOUSE_PATTERN, HUT_PATTERN };
|
||||
export { generated, surfaceGyAt, genColumn, regenerateVisibleChunks, ensureGenAroundCamera, seededRandom };
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
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';
|
||||
|
||||
// Физика жидкости
|
||||
|
|
@ -18,8 +19,6 @@ 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;
|
||||
|
|
@ -124,4 +123,4 @@ function updateWaterPhysics(dt) {
|
|||
}
|
||||
}
|
||||
|
||||
export { updateWaterPhysics };
|
||||
export { updateWaterPhysics };
|
||||
|
|
@ -1,171 +1,29 @@
|
|||
// ==================== ПОГОДА (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;
|
||||
|
||||
// Определяем биом по позиции игрока
|
||||
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');
|
||||
// Выбираем новую погоду: 40% дождь днём, 25% дождь ночью, остальное ясно
|
||||
const nightChance = isNight() ? 0.25 : 0.40;
|
||||
state.isRaining = Math.random() < nightChance;
|
||||
}
|
||||
|
||||
// Плавная интерполяция интенсивности дождя
|
||||
const rainTarget = (state.weatherType === 'rain' || state.weatherType === 'storm') ? 0.6 : 0;
|
||||
state.rainIntensity += (rainTarget - state.rainIntensity) * dt * 0.5;
|
||||
// Плавная интерполяция интенсивности
|
||||
const target = state.isRaining ? (0.4 + Math.random() * 0.01) : 0;
|
||||
state.rainIntensity += (target - 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) {
|
||||
if (state.raindrops.length > 0) {
|
||||
state.raindrops.splice(0, Math.ceil(state.raindrops.length * dt * 3));
|
||||
}
|
||||
state.raindrops.length = 0;
|
||||
return;
|
||||
}
|
||||
const spawnRate = Math.floor(state.rainIntensity * 60 * dt);
|
||||
// Спавн капель
|
||||
const spawnRate = Math.floor(state.rainIntensity * 60 * dt); // ~60 капель/сек при интенсити=1
|
||||
for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) {
|
||||
state.raindrops.push({
|
||||
x: state.camX + Math.random() * state.W,
|
||||
|
|
@ -174,47 +32,28 @@ 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 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();
|
||||
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);
|
||||
}
|
||||
|
||||
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 };
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import { BLOCKS } from '../data/blocks.js';
|
||||
import { state } from '../core/state.js';
|
||||
|
||||
// Мир-хранилище — используем state.grid и state.blocks для единого источника данных
|
||||
// Это гарантирует, что все модули (включая loop.js) видят одни и те же данные
|
||||
const grid = state.grid; // Map: key "gx,gy" => {gx,gy,t, ...}
|
||||
const blocks = state.blocks; // Array для рендера/перебора видимых
|
||||
// Мир-хранилище
|
||||
const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...}
|
||||
const blocks = []; // для рендера/перебора видимых
|
||||
|
||||
function k(gx, gy) { return gx + ',' + gy; }
|
||||
|
||||
|
|
@ -55,4 +54,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 };
|
||||
|
|
@ -0,0 +1,636 @@
|
|||
// Worms-Grechka: абстрактная симуляция боя. Ноль импортов, ноль DOM.
|
||||
// Один и тот же файл едет в клиент (хотсит) и в node-сервер (авторитет).
|
||||
// Сетка: адаптер { isSolid(gx,gy), isFluid(gx,gy), unbreakable(gx,gy),
|
||||
// remove(gx,gy)->bool, waterYPx, xMinPx, xMaxPx, killYPx }.
|
||||
const STILE = 40;
|
||||
const SGRAV = 2200; // как в Гречке
|
||||
const SMOVE = 320;
|
||||
const SJUMP = 760;
|
||||
const WORM_W = 34, WORM_H = 34;
|
||||
|
||||
const WEAPONS = {
|
||||
bazooka: { name: 'Базука', kind: 'aimed', power: true, windK: 1.0, dmg: 45, blast: 2.3, ammo: Infinity },
|
||||
homing: { name: 'Самонавод', kind: 'target', power: false, windK: 0, dmg: 50, blast: 2.3, ammo: 2 },
|
||||
sheep: { name: 'Овца', kind: 'sheep', power: false, windK: 0, dmg: 55, blast: 2.6, ammo: 1 },
|
||||
grenade: { name: 'Граната', kind: 'thrown', power: true, windK: 0.25, dmg: 40, blast: 2.1, fuse: [1, 2, 3, 4, 5], ammo: Infinity },
|
||||
cluster: { name: 'Кластер', kind: 'thrown', power: true, windK: 0.5, dmg: 26, blast: 1.5, fuse: [2, 3, 4], ammo: 3 },
|
||||
shotgun: { name: 'Дробовик', kind: 'hitscan', shots: 2, windK: 0, dmg: 6, pellets: 5, ammo: 3 },
|
||||
uzi: { name: 'Узи', kind: 'spray', windK: 0, dmg: 4, pellets: 12, ammo: 3 },
|
||||
dynamite: { name: 'Динамит', kind: 'drop', windK: 0, dmg: 70, blast: 3.6, fuse: 5, ammo: 2 },
|
||||
mine: { name: 'Мина', kind: 'drop', windK: 0, dmg: 45, blast: 2.2, ammo: 3 },
|
||||
airstrike:{ name: 'Авиаудар', kind: 'target', windK: 0, dmg: 35, blast: 1.7, ammo: 2 },
|
||||
teleport: { name: 'Телепорт', kind: 'teleport', windK: 0, ammo: 2 },
|
||||
shovel: { name: 'Лопата', kind: 'dig', windK: 0, ammo: Infinity },
|
||||
skip: { name: 'Пропуск', kind: 'skip', windK: 0, ammo: Infinity },
|
||||
};
|
||||
|
||||
const TURN_TIME = 30;
|
||||
const RETREAT_TIME = 3;
|
||||
const PROJ_GRAV = 950; // снаряды более «плавучие», чем червяки
|
||||
const WIND_ACCEL = 55; // px/s^2 на единицу ветра
|
||||
const MAX_WIND = 5;
|
||||
|
||||
function mulberry(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
class Sim {
|
||||
constructor(grid, seed) {
|
||||
this.g = grid;
|
||||
this.rand = mulberry(seed >>> 0);
|
||||
this.worms = [];
|
||||
this.proj = [];
|
||||
this.mines = [];
|
||||
this.dyn = [];
|
||||
this.strikes = [];
|
||||
this.events = [];
|
||||
this.wind = 0;
|
||||
this._wid = 1; this._pid = 1;
|
||||
}
|
||||
ev(e) { this.events.push(e); }
|
||||
drainEvents() { const e = this.events; this.events = []; return e; }
|
||||
|
||||
addWorm(team, name, x, y) {
|
||||
const w = {
|
||||
id: this._wid++, team, name, x, y, w: WORM_W, h: WORM_H,
|
||||
vx: 0, vy: 0, dir: team === 0 ? 1 : -1,
|
||||
hp: 100, alive: true, grounded: false, moveDir: 0, fallStartY: y,
|
||||
inWater: false, o2: 100, o2acc: 0,
|
||||
};
|
||||
this.worms.push(w);
|
||||
return w;
|
||||
}
|
||||
wormById(id) { return this.worms.find((w) => w.id === id); }
|
||||
aliveTeams() { const s = new Set(); for (const w of this.worms) if (w.alive) s.add(w.team); return [...s]; }
|
||||
|
||||
// --- тайловая AABB-физика в стиле Гречки ---
|
||||
solidBox(x, y, w, h) {
|
||||
const g = this.g;
|
||||
const gx0 = Math.floor(x / STILE), gx1 = Math.floor((x + w - 1) / STILE);
|
||||
const gy0 = Math.floor(y / STILE), gy1 = Math.floor((y + h - 1) / STILE);
|
||||
for (let gy = gy0; gy <= gy1; gy++)
|
||||
for (let gx = gx0; gx <= gx1; gx++)
|
||||
if (g.isSolid(gx, gy)) return true;
|
||||
return false;
|
||||
}
|
||||
fluidBox(x, y, w, h) {
|
||||
const g = this.g;
|
||||
const gx0 = Math.floor(x / STILE), gx1 = Math.floor((x + w - 1) / STILE);
|
||||
const gy0 = Math.floor(y / STILE), gy1 = Math.floor((y + h - 1) / STILE);
|
||||
for (let gy = gy0; gy <= gy1; gy++)
|
||||
for (let gx = gx0; gx <= gx1; gx++)
|
||||
if (g.isFluid(gx, gy)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
setMove(id, dir) { const w = this.wormById(id); if (w && w.alive) { w.moveDir = dir; if (dir) w.dir = dir; } }
|
||||
jump(id) {
|
||||
const w = this.wormById(id);
|
||||
if (w && w.alive && (w.grounded || w.inWater)) {
|
||||
w.vy = w.inWater ? -SJUMP * 0.55 : -SJUMP; // в воде — гребок вверх
|
||||
w.grounded = false;
|
||||
}
|
||||
}
|
||||
|
||||
hurt(w, dmg, cause) {
|
||||
if (!w.alive) return;
|
||||
w.hp -= dmg;
|
||||
this.ev({ t: 'damage', worm: w.id, dmg: Math.round(dmg), hp: Math.max(0, Math.round(w.hp)) });
|
||||
if (w.hp <= 0) this.kill(w, cause);
|
||||
}
|
||||
kill(w, cause) {
|
||||
if (!w.alive) return;
|
||||
w.alive = false; w.hp = 0; w.moveDir = 0;
|
||||
this.ev({ t: 'die', worm: w.id, team: w.team, cause: cause || 'killed', x: Math.round(w.x), y: Math.round(w.y) });
|
||||
}
|
||||
|
||||
explodePx(x, y, dmg, blastTiles, cause) {
|
||||
const g = this.g;
|
||||
const R = blastTiles * STILE;
|
||||
// разрушение: круг по тайлам (жидкости и неразрушаемое не трогаем)
|
||||
const removed = [];
|
||||
const gx0 = Math.floor((x - R) / STILE), gx1 = Math.floor((x + R) / STILE);
|
||||
const gy0 = Math.floor((y - R) / STILE), gy1 = Math.floor((y + R) / STILE);
|
||||
for (let gy = gy0; gy <= gy1; gy++)
|
||||
for (let gx = gx0; gx <= gx1; gx++) {
|
||||
const cx = gx * STILE + STILE / 2, cy = gy * STILE + STILE / 2;
|
||||
if (Math.hypot(cx - x, cy - y) > R) continue;
|
||||
// взрыв на суше не роет ниже уровня моря — в воду ничего не падает
|
||||
if (y < g.waterYPx && gy * STILE > g.waterYPx) continue;
|
||||
if (g.isFluid(gx, gy) || g.unbreakable(gx, gy)) continue;
|
||||
if (g.remove(gx, gy)) removed.push([gx, gy]);
|
||||
}
|
||||
this.ev({ t: 'carve', x: Math.round(x), y: Math.round(y), r: Math.round(R), tiles: removed });
|
||||
this.ev({ t: 'explode', x: Math.round(x), y: Math.round(y), r: Math.round(R), dmg });
|
||||
// урон + отброс
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive) continue;
|
||||
const dx = (w.x + w.w / 2) - x, dy = (w.y + w.h / 2) - y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist < R + 60) {
|
||||
const dd = Math.max(1, Math.round(dmg * (1 - dist / (R + 60))));
|
||||
this.hurt(w, dd, cause || 'boom');
|
||||
// отброс с ограничением: серия взрывов не катапультирует через полкарты в воду
|
||||
w.vx = Math.max(-450, Math.min(450, w.vx + (dx / (dist || 1)) * 620));
|
||||
w.vy = Math.max(-450, w.vy - 330);
|
||||
w.grounded = false;
|
||||
}
|
||||
}
|
||||
// цепные реакции
|
||||
for (const m of this.mines)
|
||||
if (m.fuse <= 0 && Math.hypot(m.x - x, m.y - y) < R) { m.fuse = 0.25; m.armed = 1; }
|
||||
for (const d of this.dyn)
|
||||
if (Math.hypot(d.x - x, d.y - y) < R) d.fuse = Math.min(d.fuse, 0.4);
|
||||
for (const p of this.proj)
|
||||
if ((p.kind === 'grenade' || p.kind === 'cluster') && Math.hypot(p.x - x, p.y - y) < R)
|
||||
p.fuse = Math.min(p.fuse, 0.05);
|
||||
}
|
||||
|
||||
fire(wormId, weaponId, p) {
|
||||
const w = this.wormById(wormId);
|
||||
const def = WEAPONS[weaponId];
|
||||
if (!w || !w.alive || !def) return false;
|
||||
p = p || {};
|
||||
const ang = p.angle || 0;
|
||||
const pow = Math.max(0, Math.min(1, p.power == null ? 0.5 : p.power));
|
||||
const cx = w.x + w.w / 2, cy = w.y + w.h / 2;
|
||||
const mx = cx + Math.cos(ang) * 26, my = cy - 6 + Math.sin(ang) * 26;
|
||||
const P = (kind, x, y, vx, vy, extra) => Object.assign({ id: this._pid++, kind, x, y, vx, vy, age: 0 }, extra || {});
|
||||
switch (def.kind) {
|
||||
case 'aimed': {
|
||||
const sp = 500 + pow * 1000;
|
||||
this.proj.push(P(weaponId === 'homing' ? 'homing' : 'missile', mx, my, Math.cos(ang) * sp, Math.sin(ang) * sp,
|
||||
{ dmg: def.dmg, blast: def.blast, windK: def.windK, owner: w.id, team: w.team, life: weaponId === 'homing' ? 5 : 0 }));
|
||||
break;
|
||||
}
|
||||
case 'sheep': {
|
||||
// овцу выпускаем у ног — бежит сама, прыгает через уступы, разворачивается у стен
|
||||
this.proj.push(P('sheep', cx + w.dir * 24, cy + 10, w.dir * 60, -60,
|
||||
{ dmg: def.dmg, blast: def.blast, fuse: 4, owner: w.id, team: w.team, dir: w.dir }));
|
||||
break;
|
||||
}
|
||||
case 'thrown': {
|
||||
const sp = 300 + pow * 800;
|
||||
const fuse = p.fuse || (def.fuse ? def.fuse[2] : 3);
|
||||
this.proj.push(P(weaponId === 'cluster' ? 'cluster' : 'grenade', cx, cy - 20,
|
||||
w.dir * 60 + Math.cos(ang) * sp * 0.7, Math.sin(ang) * sp - 140,
|
||||
{ dmg: def.dmg, blast: def.blast, windK: def.windK, fuse, owner: w.id, team: w.team }));
|
||||
break;
|
||||
}
|
||||
case 'hitscan': {
|
||||
for (let i = 0; i < def.pellets; i++) {
|
||||
const a = ang + (this.rand() - 0.5) * 0.14;
|
||||
this.proj.push(P('pellet', mx, my, Math.cos(a) * 1500, Math.sin(a) * 1500,
|
||||
{ dmg: def.dmg, life: 1.6, owner: w.id, team: w.team }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'spray': {
|
||||
for (let i = 0; i < def.pellets; i++) {
|
||||
const a = ang + (this.rand() - 0.5) * 0.26;
|
||||
const sp = 1250 + this.rand() * 250;
|
||||
this.proj.push(P('pellet', mx, my, Math.cos(a) * sp, Math.sin(a) * sp,
|
||||
{ dmg: def.dmg, life: 1.4, owner: w.id, team: w.team }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'drop': {
|
||||
if (weaponId === 'dynamite') this.dyn.push({ x: cx + w.dir * 30, y: cy, fuse: def.fuse });
|
||||
else this.mines.push({ x: cx + w.dir * 30, y: cy, team: w.team, armed: 0, fuse: 0 });
|
||||
break;
|
||||
}
|
||||
case 'target': {
|
||||
const tx = Math.max(this.g.xMinPx + 30, Math.min(this.g.xMaxPx - 30, p.x || (cx + w.dir * 200)));
|
||||
if (weaponId === 'homing') {
|
||||
// самонавод: ракета вылетает и летит в отмеченную крестиком точку.
|
||||
// стартуем из первой свободной клетки вдоль прицела — иначе нос в стене = взрыв в упор
|
||||
const tgtY = p.y == null ? cy : p.y;
|
||||
const a0 = Math.atan2(tgtY - my, tx - mx);
|
||||
let hx = mx, hy = my;
|
||||
for (let d = 8; d <= 80; d += 8) {
|
||||
const qx = mx + Math.cos(a0) * d, qy = my + Math.sin(a0) * d;
|
||||
if (!this.pointSolid(qx, qy)) { hx = qx; hy = qy; break; }
|
||||
}
|
||||
this.proj.push(P('homing', hx, hy, Math.cos(a0) * 950, Math.sin(a0) * 950,
|
||||
{ dmg: def.dmg, blast: def.blast, windK: 0, owner: w.id, team: w.team, life: 6, tx, ty: tgtY }));
|
||||
} else {
|
||||
this.strikes.push({ x: tx, t: 0.8, left: 5, next: 0, dir: this.rand() < 0.5 ? -1 : 1, px: 0, py: 0, started: false, team: w.team });
|
||||
}
|
||||
this.ev({ t: 'strike_incoming', x: Math.round(tx) });
|
||||
break;
|
||||
}
|
||||
case 'teleport': {
|
||||
const tx = Math.max(this.g.xMinPx + 20, Math.min(this.g.xMaxPx - 20, p.x || cx));
|
||||
// ищем землю под точкой
|
||||
const gy = this.g.groundBelowPx(tx, p.y == null ? 0 : p.y);
|
||||
if (gy !== null) {
|
||||
w.x = tx - w.w / 2; w.y = gy - w.h - 1; w.vx = 0; w.vy = 0;
|
||||
this.ev({ t: 'teleport', worm: w.id, x: Math.round(w.x), y: Math.round(w.y) });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'dig': {
|
||||
// лопата: рейкаст вдоль прицела — копаем первую твёрдую землю (вперёд/назад/вниз)
|
||||
let hx = null, hy = null;
|
||||
const dx = Math.cos(ang), dy = Math.sin(ang);
|
||||
for (let d = 8; d <= 95 && hx == null; d += 8) {
|
||||
const gx = Math.floor((cx + dx * d) / STILE), gy = Math.floor((cy + dy * d) / STILE);
|
||||
if (this.g.unbreakable(gx, gy) || this.g.isFluid(gx, gy)) continue;
|
||||
if (this.g.isSolid ? this.g.isSolid(gx, gy) : true) { hx = gx; hy = gy; }
|
||||
}
|
||||
// не зацепили — опускаемся от точки прицела вниз до земли
|
||||
let px = cx + dx * 52, py = cy + dy * 52;
|
||||
if (hx == null) {
|
||||
for (let dd = 0; dd <= 120; dd += 10) {
|
||||
const gx = Math.floor(px / STILE), gy = Math.floor((py + dd) / STILE);
|
||||
if (this.g.unbreakable(gx, gy) || this.g.isFluid(gx, gy)) continue;
|
||||
if (this.g.isSolid ? this.g.isSolid(gx, gy) : true) { px = gx * STILE + STILE / 2; py = gy * STILE + STILE / 2; hx = gx; hy = gy; break; }
|
||||
}
|
||||
if (hx == null) return false; // рядом нет земли — пустой взмах
|
||||
} else { px = hx * STILE + STILE / 2; py = hy * STILE + STILE / 2; }
|
||||
const R = STILE * 1.6; // тоннели просторные — внутри можно ходить и прыгать
|
||||
let dug = 0;
|
||||
const gx0 = Math.floor((px - R) / STILE), gx1 = Math.floor((px + R) / STILE);
|
||||
const gy0 = Math.floor((py - R) / STILE), gy1 = Math.floor((py + R) / STILE);
|
||||
for (let gy = gy0; gy <= gy1; gy++)
|
||||
for (let gx = gx0; gx <= gx1; gx++) {
|
||||
const bx = gx * STILE + STILE / 2, by = gy * STILE + STILE / 2;
|
||||
if (Math.hypot(bx - px, by - py) > R) continue;
|
||||
if (this.g.isFluid(gx, gy) || this.g.unbreakable(gx, gy)) continue;
|
||||
if (this.g.remove(gx, gy)) dug++;
|
||||
}
|
||||
if (dug > 0) this.ev({ t: 'dig', x: Math.round(px), y: Math.round(py) });
|
||||
return dug > 0;
|
||||
}
|
||||
case 'skip': break;
|
||||
}
|
||||
w.moveDir = 0;
|
||||
this.ev({ t: 'fired', worm: w.id, weapon: weaponId });
|
||||
return true;
|
||||
}
|
||||
|
||||
step(dt) {
|
||||
const g = this.g;
|
||||
// --- червяки ---
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive) continue;
|
||||
const cx = w.x + w.w / 2, cy = w.y + w.h / 2;
|
||||
// вода: можно плыть (как в Гречке), смерть только от нехватки кислорода
|
||||
const wasWater = w.inWater;
|
||||
w.inWater = this.fluidBox(w.x + 4, w.y + 4, w.w - 8, w.h - 8);
|
||||
if (w.inWater && !wasWater) this.ev({ t: 'splash', x: Math.round(cx) });
|
||||
const headWet = this.fluidBox(w.x + 8, w.y, w.w - 16, 14);
|
||||
if (headWet) {
|
||||
w.o2 = Math.max(0, w.o2 - 14 * dt);
|
||||
if (w.o2 <= 0) {
|
||||
w.o2acc += 12 * dt;
|
||||
if (w.o2acc >= 5) { w.o2acc -= 5; this.hurt(w, 5, 'drown'); }
|
||||
}
|
||||
} else w.o2 = Math.min(100, w.o2 + 50 * dt);
|
||||
if (w.y > g.killYPx) { this.kill(w, 'out'); continue; }
|
||||
// горизонталь (в воде медленнее)
|
||||
const spd = w.inWater ? SMOVE * 0.5 : SMOVE;
|
||||
w.vx = w.moveDir * spd;
|
||||
if (w.vx !== 0) {
|
||||
let nx = w.x + w.vx * dt;
|
||||
nx = Math.max(g.xMinPx, Math.min(g.xMaxPx - w.w, nx));
|
||||
if (!this.solidBox(nx, w.y + 2, w.w, w.h - 2)) w.x = nx;
|
||||
else {
|
||||
// подъём по склону до 1 тайла
|
||||
let stepped = false;
|
||||
for (let k = 8; k <= STILE; k += 8) {
|
||||
if (!this.solidBox(nx, w.y - k, w.w, w.h)) { w.x = nx; w.y -= k; stepped = true; break; }
|
||||
}
|
||||
if (!stepped) w.vx = 0;
|
||||
}
|
||||
}
|
||||
// вертикаль (в воде — плавучесть как в Гречке)
|
||||
w.vy += (w.inWater ? 550 : SGRAV) * dt;
|
||||
if (w.inWater && w.vy > 260) w.vy = 260;
|
||||
if (w.vy > 1600) w.vy = 1600;
|
||||
let ny = w.y + w.vy * dt;
|
||||
if (w.vy >= 0) {
|
||||
// опора под ногами
|
||||
if (this.solidBox(w.x + 2, ny + w.h - 6, w.w - 4, 8)) {
|
||||
const gy = Math.floor((ny + w.h) / STILE);
|
||||
w.y = gy * STILE - w.h;
|
||||
const fallTiles = (w.y - w.fallStartY) / STILE;
|
||||
if (fallTiles > 6 && !this.fluidBox(w.x, w.y, w.w, w.h)) { console.log('[DBG] fall worm=' + w.id + ' tiles=' + fallTiles.toFixed(1)); this.hurt(w, (fallTiles - 6) * 10, 'fall'); }
|
||||
w.vy = 0; w.grounded = true; w.fallStartY = w.y;
|
||||
} else { w.y = ny; w.grounded = false; }
|
||||
} else {
|
||||
if (this.solidBox(w.x + 2, ny, w.w - 4, 8)) { w.vy = 0; }
|
||||
else { w.y = ny; w.grounded = false; }
|
||||
}
|
||||
if (!w.grounded && this.solidBox(w.x + 2, w.y + w.h, w.w - 4, 3)) {
|
||||
w.grounded = true; w.vy = 0; w.fallStartY = w.y;
|
||||
}
|
||||
}
|
||||
// --- мины ---
|
||||
for (const m of this.mines) {
|
||||
if (m.armed < 1 && m.fuse <= 0) m.armed = Math.min(1, m.armed + dt / 2);
|
||||
else if (m.fuse > 0) {
|
||||
m.fuse -= dt;
|
||||
if (m.fuse <= 0) { this.explodePx(m.x, m.y, WEAPONS.mine.dmg, WEAPONS.mine.blast, 'mine'); m.dead = true; }
|
||||
} else {
|
||||
for (const w of this.worms) {
|
||||
if (w.alive && w.team !== m.team &&
|
||||
Math.hypot(w.x + w.w / 2 - m.x, w.y + w.h / 2 - m.y) < 46) {
|
||||
m.fuse = 0.8; this.ev({ t: 'mine_beep', x: Math.round(m.x), y: Math.round(m.y) }); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.mines = this.mines.filter((m) => !m.dead);
|
||||
// --- динамит ---
|
||||
for (const d of this.dyn) {
|
||||
d.fuse -= dt;
|
||||
if (d.fuse <= 0) { this.explodePx(d.x, d.y, WEAPONS.dynamite.dmg, WEAPONS.dynamite.blast, 'dynamite'); d.dead = true; }
|
||||
}
|
||||
this.dyn = this.dyn.filter((d) => !d.dead);
|
||||
// --- авиаудары: пиксельный самолёт пролетает и сбрасывает бомбы ---
|
||||
for (const s of this.strikes) {
|
||||
if (!s.started) {
|
||||
s.t -= dt;
|
||||
if (s.t <= 0) {
|
||||
s.started = true;
|
||||
s.px = s.dir > 0 ? this.g.xMinPx - 300 : this.g.xMaxPx + 300;
|
||||
s.py = -170; // выше самых высоких деревьев на пиках (пик 0 + дерево 80 + запас)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
s.px += s.dir * 560 * dt;
|
||||
const inZone = Math.abs(s.px - s.x) < 260;
|
||||
const passed = s.dir > 0 ? s.px > s.x + 200 : s.px < s.x - 200;
|
||||
if (s.left > 0 && (inZone || passed)) {
|
||||
s.next -= dt;
|
||||
if (s.next <= 0) {
|
||||
s.next = 0.16; s.left--;
|
||||
this.proj.push({ id: this._pid++, kind: 'striker', x: s.px, y: s.py + 10, vx: s.dir * 120, vy: 200, age: 0, dmg: WEAPONS.airstrike.dmg, blast: WEAPONS.airstrike.blast, windK: 0, team: s.team });
|
||||
}
|
||||
}
|
||||
if (s.left <= 0 && Math.abs(s.px - s.x) > 600) s.dead = true;
|
||||
}
|
||||
this.strikes = this.strikes.filter((s) => !s.dead);
|
||||
// --- снаряды ---
|
||||
for (const p of this.proj) {
|
||||
p.age += dt;
|
||||
if (p.kind === 'pellet') {
|
||||
p.life -= dt;
|
||||
// субстепы ≤10px: иначе быстрая пуля пролетает червяка насквозь
|
||||
const dist = Math.hypot(p.vx * dt, p.vy * dt);
|
||||
const n = Math.max(1, Math.ceil(dist / 10));
|
||||
for (let k = 0; k < n && !p.dead; k++) {
|
||||
if (p.life <= 0 || this.advance(p, dt / n)) { p.dead = true; break; }
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive || (w.id === p.owner && p.age < 0.05)) continue;
|
||||
if (p.x > w.x && p.x < w.x + w.w && p.y > w.y && p.y < w.y + w.h) {
|
||||
this.hurt(w, p.dmg, 'shot');
|
||||
w.vx = 0; w.vy = -120; w.grounded = false;
|
||||
p.dead = true; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p.dead) continue;
|
||||
} else if (p.kind === 'striker') {
|
||||
p.vy += PROJ_GRAV * 0.5 * dt;
|
||||
if (this.advance(p, dt)) { this.explodePx(p.x, p.y, p.dmg, p.blast, 'air'); p.dead = true; }
|
||||
} else if (p.kind === 'missile') {
|
||||
p.vx += this.wind * WIND_ACCEL * (p.windK || 0) * dt;
|
||||
p.vy += PROJ_GRAV * dt;
|
||||
if (this.advance(p, dt)) { this.explodePx(p.x, p.y, p.dmg, p.blast, 'bazooka'); p.dead = true; continue; }
|
||||
// вода — только настоящая (клетка), а не «всё ниже уровня моря»: сухие катакомбы под морем — не вода
|
||||
if (this.g.isFluid(Math.floor(p.x / STILE), Math.floor(p.y / STILE))) {
|
||||
// упала в воду — взрыв на поверхности, а не тихое бульк
|
||||
this.ev({ t: 'splash', x: Math.round(p.x) });
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'bazooka');
|
||||
p.dead = true; continue;
|
||||
}
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive || (w.id === p.owner && p.age < 0.1)) continue;
|
||||
if (Math.hypot(w.x + w.w / 2 - p.x, w.y + w.h / 2 - p.y) < 26) {
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'bazooka'); p.dead = true; break;
|
||||
}
|
||||
}
|
||||
} else if (p.kind === 'homing') {
|
||||
// самонаводящаяся ракета: летит в отмеченную крестиком точку (или доворачивает к врагу рядом с ней)
|
||||
p.life -= dt;
|
||||
let tgt = null, bd = 1e9;
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive || w.team === p.team) continue;
|
||||
const d = Math.hypot(w.x + w.w / 2 - p.x, w.y + w.h / 2 - p.y);
|
||||
if (d < bd) { bd = d; tgt = w; }
|
||||
}
|
||||
// враг далеко от точки — летим строго в точку; рядом с точкой — доворачиваем к нему
|
||||
const tdx = (p.tx == null ? p.x : p.tx), tdy = (p.ty == null ? p.y : p.ty);
|
||||
let gx = tdx, gy = tdy;
|
||||
if (tgt && Math.hypot(tgt.x - tdx, tgt.y - tdy) < 120) { gx = tgt.x + tgt.w / 2; gy = tgt.y + tgt.h / 2; }
|
||||
const SPD = 950;
|
||||
{
|
||||
const cur = Math.atan2(p.vy, p.vx);
|
||||
const want = Math.atan2(gy - p.y, gx - p.x);
|
||||
let d = want - cur;
|
||||
while (d > Math.PI) d -= Math.PI * 2;
|
||||
while (d < -Math.PI) d += Math.PI * 2;
|
||||
const turn = 3.4 * dt, na = cur + Math.max(-turn, Math.min(turn, d));
|
||||
p.vx = Math.cos(na) * SPD; p.vy = Math.sin(na) * SPD;
|
||||
}
|
||||
if (Math.hypot(gx - p.x, gy - p.y) < 30) { this.explodePx(p.x, p.y, p.dmg, p.blast, 'homing'); p.dead = true; continue; }
|
||||
if (this.advance(p, dt)) {
|
||||
if (p.age < 0.12) continue; // только вылетела — нос ещё выходит из-за укрытия, не взрываем
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'homing'); p.dead = true; continue;
|
||||
}
|
||||
if (p.life <= 0) { this.explodePx(p.x, p.y, p.dmg, p.blast, 'homing'); p.dead = true; continue; }
|
||||
if (this.g.isFluid(Math.floor(p.x / STILE), Math.floor(p.y / STILE))) {
|
||||
this.ev({ t: 'splash', x: Math.round(p.x) });
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'homing');
|
||||
p.dead = true; continue;
|
||||
}
|
||||
for (const w of this.worms) {
|
||||
if (!w.alive || (w.id === p.owner && p.age < 0.1)) continue;
|
||||
if (Math.hypot(w.x + w.w / 2 - p.x, w.y + w.h / 2 - p.y) < 26) {
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'homing'); p.dead = true; break;
|
||||
}
|
||||
}
|
||||
} else if (p.kind === 'sheep') {
|
||||
// супер-овца: бежит по земле, вскарабкивается на уступы, у стен разворачивается, у врага — бум
|
||||
p.fuse -= dt;
|
||||
p.vy = Math.min(900, p.vy + SGRAV * dt);
|
||||
const dir = p.dir || 1, nx = p.x + dir * 150 * dt;
|
||||
if (!this.pointSolid(nx, p.y)) p.x = nx;
|
||||
else if (!this.pointSolid(nx, p.y - 34)) { p.x = nx; p.y -= 34; p.vy = Math.min(0, p.vy); }
|
||||
else p.dir = -dir;
|
||||
p.y += p.vy * dt;
|
||||
if (this.pointSolid(p.x, p.y + 10)) {
|
||||
let n = 0;
|
||||
while (n++ < 20 && this.pointSolid(p.x, p.y + 8)) p.y -= 2;
|
||||
p.vy = 0;
|
||||
}
|
||||
if (p.y > this.g.killYPx) { p.dead = true; continue; }
|
||||
if (this.g.isFluid(Math.floor(p.x / STILE), Math.floor(p.y / STILE))) {
|
||||
this.ev({ t: 'splash', x: Math.round(p.x) });
|
||||
this.explodePx(p.x, p.y, p.dmg, p.blast, 'sheep');
|
||||
p.dead = true; continue;
|
||||
}
|
||||
let hit = p.fuse <= 0;
|
||||
if (!hit) for (const w of this.worms) {
|
||||
if (!w.alive || w.team === p.team) continue; // своих (включая хозяина) не задевает — только враги
|
||||
if (Math.hypot(w.x + w.w / 2 - p.x, w.y + w.h / 2 - p.y) < 30) { hit = true; break; }
|
||||
}
|
||||
if (hit) { this.explodePx(p.x, p.y, p.dmg, p.blast, 'sheep'); p.dead = true; }
|
||||
} else { // grenade / cluster
|
||||
p.vx += this.wind * WIND_ACCEL * (p.windK || 0) * dt;
|
||||
p.vy += PROJ_GRAV * dt;
|
||||
this.bounce(p, dt);
|
||||
p.fuse -= dt;
|
||||
// вода — только настоящая клетка: в сухих катакомбах под морем граната летит дальше
|
||||
if (this.g.isFluid(Math.floor(p.x / STILE), Math.floor(p.y / STILE))) {
|
||||
// граната/кластер в воде — взрыв на месте + осколки, а не тихое бульк
|
||||
this.ev({ t: 'splash', x: Math.round(p.x) });
|
||||
const cy = p.y;
|
||||
if (p.kind === 'cluster') {
|
||||
this.explodePx(p.x, cy, 10, 1.1, 'cluster');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const a = (i / 5) * Math.PI * 2 + this.rand() * 0.6;
|
||||
this.proj.push({ id: this._pid++, kind: 'grenade', x: p.x, y: cy - 4, vx: Math.cos(a) * (160 + this.rand() * 180), vy: Math.sin(a) * 240 - 180, age: 0, dmg: p.dmg, blast: WEAPONS.cluster.blast, windK: 0.3, fuse: 1.0, owner: p.owner });
|
||||
}
|
||||
} else this.explodePx(p.x, cy, p.dmg, p.blast, 'grenade');
|
||||
p.dead = true; continue;
|
||||
}
|
||||
if (p.fuse <= 0) {
|
||||
if (p.kind === 'cluster') {
|
||||
this.explodePx(p.x, p.y, 10, 1.1, 'cluster');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const a = (i / 5) * Math.PI * 2 + this.rand() * 0.6;
|
||||
this.proj.push({ id: this._pid++, kind: 'grenade', x: p.x, y: p.y - 4, vx: Math.cos(a) * (160 + this.rand() * 180), vy: Math.sin(a) * 240 - 180, age: 0, dmg: p.dmg, blast: WEAPONS.cluster.blast, windK: 0.3, fuse: 1.0, owner: p.owner });
|
||||
}
|
||||
} else this.explodePx(p.x, p.y, p.dmg, p.blast, 'grenade');
|
||||
p.dead = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.proj = this.proj.filter((p) => !p.dead);
|
||||
}
|
||||
|
||||
pointSolid(x, y) {
|
||||
return this.g.isSolid(Math.floor(x / STILE), Math.floor(y / STILE));
|
||||
}
|
||||
advance(p, dt) {
|
||||
// Возвращает true при ударе о землю (dead НЕ ставит — решает вызывающий:
|
||||
// пуля гаснет, базука взрывается).
|
||||
const dist = Math.hypot(p.vx * dt, p.vy * dt);
|
||||
const n = Math.max(1, Math.ceil(dist / 6));
|
||||
for (let i = 0; i < n; i++) {
|
||||
p.x += (p.vx * dt) / n; p.y += (p.vy * dt) / n;
|
||||
if (this.pointSolid(p.x, p.y)) { p.hitX = p.x; p.hitY = p.y; return true; }
|
||||
if (p.x < this.g.xMinPx + 2 || p.x > this.g.xMaxPx - 2) { p.vx = -p.vx; p.x = Math.max(this.g.xMinPx + 2, Math.min(this.g.xMaxPx - 2, p.x)); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bounce(p, dt) {
|
||||
const dist = Math.hypot(p.vx * dt, p.vy * dt);
|
||||
const n = Math.max(1, Math.ceil(dist / 5));
|
||||
let hitX = false, hitY = false;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const sx = (p.vx * dt) / n, sy = (p.vy * dt) / n;
|
||||
if (!this.pointSolid(p.x + sx, p.y)) p.x += sx; else hitX = true;
|
||||
if (!this.pointSolid(p.x, p.y + sy)) p.y += sy; else hitY = true;
|
||||
}
|
||||
// отклик — один раз за шаг, а не за каждый подшаг: иначе касание склона
|
||||
// на скорости гасит скорость в ноль и граната падает сильно ближе прицела
|
||||
if (hitX) p.vx = -p.vx * 0.45;
|
||||
if (hitY) { p.vy = -p.vy * 0.42; p.vx *= 0.82; if (Math.abs(p.vy) < 70) p.vy = 0; }
|
||||
if (p.vy === 0 && Math.abs(p.vx) < 30) p.vx = 0;
|
||||
if (p.y < 0) { p.y = 0; p.vy = Math.abs(p.vy) * 0.5; }
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
return {
|
||||
worms: this.worms.map((w) => ({ id: w.id, team: w.team, name: w.name, x: Math.round(w.x * 10) / 10, y: Math.round(w.y * 10) / 10, dir: w.dir, hp: Math.round(w.hp), alive: w.alive, swim: w.inWater, o2: Math.round(w.o2) })),
|
||||
proj: this.proj.map((p) => ({ id: p.id, k: p.kind, x: Math.round(p.x), y: Math.round(p.y), vx: Math.round(p.vx || 0), vy: Math.round(p.vy || 0), t: p.team, o: p.owner })),
|
||||
mines: this.mines.map((m) => ({ x: Math.round(m.x), y: Math.round(m.y), armed: m.armed >= 1, beep: m.fuse > 0 })),
|
||||
dyn: this.dyn.map((d) => ({ x: Math.round(d.x), y: Math.round(d.y) })),
|
||||
strikes: this.strikes.length,
|
||||
planes: this.strikes.filter((s) => s.started && !s.dead).map((s) => ({ x: Math.round(s.px), y: Math.round(s.py), d: s.dir, t: s.team })),
|
||||
wind: this.wind,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Перебор углов/сил через превью: куда упадёт снаряд ближе к цели.
|
||||
// Чистая функция — используется и ИИ в клиенте, и тестами.
|
||||
function solveShot(grid, mx, my, tx, ty, thrown, wind, dir) {
|
||||
let best = { ang: -1.1, pow: 0.6, err: 1e12 };
|
||||
const angs = thrown ? [-2.4, -2.0, -1.6, -1.2, -0.8] : [-2.5, -2.1, -1.7, -1.3, -0.9, -0.5];
|
||||
const pows = [0.3, 0.5, 0.7, 0.9, 1.0];
|
||||
for (const a of angs) {
|
||||
for (const p of pows) {
|
||||
const sx = mx + Math.cos(a) * 26, sy = my + Math.sin(a) * 26;
|
||||
const pts = previewShot(grid, sx, sy, a, p,
|
||||
{ thrown, wind, windK: thrown ? 0.25 : 1.0, fuse: 4, dir: dir || 1 });
|
||||
if (!pts.length) continue;
|
||||
const land = pts[pts.length - 1];
|
||||
const err = Math.hypot(land[0] - tx, land[1] - ty) + (thrown ? 0 : Math.abs(a + 1.2) * 30);
|
||||
if (err < best.err) best = { ang: a, pow: p, err };
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Превью траектории для прицела (только aimed/thrown, без взрывов по пути).
|
||||
function previewStraight(grid, sx, sy, angle, maxLen) {
|
||||
// прямой прицел дроби/УЗИ: луч без гравитации до земли или лимита
|
||||
const pts = [];
|
||||
const dx = Math.cos(angle), dy = Math.sin(angle);
|
||||
for (let d = 24; d <= (maxLen || 520); d += 24) {
|
||||
const x = sx + dx * d, y = sy + dy * d;
|
||||
pts.push([Math.round(x), Math.round(y)]);
|
||||
if (grid.isSolid(Math.floor(x / STILE), Math.floor(y / STILE))) break;
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
function previewShot(grid, sx, sy, angle, power, opts) {
|
||||
opts = opts || {};
|
||||
const pts = [];
|
||||
let x = sx, y = sy;
|
||||
let vx, vy;
|
||||
if (opts.thrown) {
|
||||
const sp = 300 + power * 800;
|
||||
vx = (opts.dir || 1) * 60 + Math.cos(angle) * sp * 0.7;
|
||||
vy = Math.sin(angle) * sp - 140;
|
||||
} else {
|
||||
vx = Math.cos(angle) * (500 + power * 1000);
|
||||
vy = Math.sin(angle) * (500 + power * 1000);
|
||||
}
|
||||
const dt = 1 / 60;
|
||||
const windAcc = (opts.wind || 0) * WIND_ACCEL * (opts.windK || 0);
|
||||
const maxT = opts.fuse || 4;
|
||||
const solid = (x, y) => grid.isSolid(Math.floor(x / STILE), Math.floor(y / STILE));
|
||||
const fluid = (x, y) => grid.isFluid(Math.floor(x / STILE), Math.floor(y / STILE));
|
||||
for (let t = 0; t < maxT; t += dt) {
|
||||
vx += windAcc * dt; vy += PROJ_GRAV * dt;
|
||||
// подшаги как в bounce/advance сима: иначе на скорости превью проваливается
|
||||
// в склон и рисует точку падения дальше, чем долетит граната
|
||||
const dist = Math.hypot(vx * dt, vy * dt);
|
||||
const n = Math.max(1, Math.ceil(dist / 5));
|
||||
let hit = false;
|
||||
for (let i = 0; i < n; i++) {
|
||||
x += (vx * dt) / n; y += (vy * dt) / n;
|
||||
if (solid(x, y) || fluid(x, y)) { hit = true; break; }
|
||||
}
|
||||
const last = pts[pts.length - 1];
|
||||
if (!last || Math.abs(x - last[0]) + Math.abs(y - last[1]) > 16) pts.push([Math.round(x), Math.round(y)]);
|
||||
if (hit || pts.length > 90) break;
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
// API для node (require) и для бандлера (общая область IIFE — без export).
|
||||
const __api = { STILE, SGRAV, SMOVE, SJUMP, WORM_W, WORM_H, WEAPONS, TURN_TIME, RETREAT_TIME, MAX_WIND, Sim, previewShot, solveShot };
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = __api;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,37 +0,0 @@
|
|||
// Звуковой движок
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ====================
|
||||
// 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. Это может вызвать проблемы.');
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
// ==================== ХОЛСТ 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; }
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
// ==================== КОНСТАНТЫ ИГРЫ ====================
|
||||
|
||||
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';
|
||||
|
|
@ -1,251 +0,0 @@
|
|||
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,
|
||||
};
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
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 },
|
||||
};
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
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:'🔱' };
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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 }
|
||||
];
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
// Лодка
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
// Моб 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] || [];
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
// Сущности: животные + зомби + новые мобы
|
||||
|
||||
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; }
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -1,548 +0,0 @@
|
|||
// ==================== ГЛАВНЫЙ ИГРОВОЙ ЦИКЛ ====================
|
||||
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);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// ==================== РЕЖИМЫ ====================
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
// ==================== СИСТЕМА СОХРАНЕНИЯ ИГРЫ ====================
|
||||
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('Игра загружена');
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// ==================== УПРАВЛЕНИЕ ====================
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
// ==================== ВЗАИМОДЕЙСТВИЕ МЫШЬ/ТАП ====================
|
||||
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
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue