Compare commits

..

15 Commits

Author SHA1 Message Date
Mk 0a31f92af8 feat: voice chat v3 — AudioWorklet, Opus/PCM, per-speaker, spatial audio, VAD 2026-05-26 17:48:01 +00:00
Mk 980ba6a541 feat: biomes, weather, new mobs, crops, structures, level unlocks 2026-05-26 17:27:10 +00:00
Mk 714e4cf162 fix: voice v3 — smaller send chunks, 200ms jbuf, smooth fade on underrun, 1.5s timeout 2026-05-26 14:08:55 +00:00
Mk 2ee82a45b0 fix: deselect items on second click, inventory toggle close, cache bust v22 2026-05-26 14:04:55 +00:00
Mk 0e12ed7a18 fix: ring buffer voice playback — continuous stream, no BufferSource per chunk 2026-05-26 13:52:18 +00:00
Mk 233ff02976 fix: jitter buffer for voice chat — 120ms delay, continuous scheduling, gain ramp 2026-05-26 13:34:12 +00:00
Mk 2ebb457fc5 feat: XP/level system, mob loot drops, level-up popup 2026-05-26 13:29:26 +00:00
Mk edb08094db fix: minimap position — right of stats panel 2026-05-26 13:17:11 +00:00
Mk 7eef966f6e fix: voice chat — fade in/out chunks, bigger buffer, volume boost to reduce bubbling 2026-05-26 13:13:13 +00:00
Mk a59c84535a fix: brighter torch/campfire light, 24 rays, wider radius, warmer glow 2026-05-26 13:10:55 +00:00
Mk 5774a41761 fix: voice btns row2 align, custom modals, panel-header css, close btn fix 2026-05-26 13:07:06 +00:00
Mk 6b6125ae81 feat: voice mode switcher (near/world) + mob fixes 2026-05-26 12:51:45 +00:00
Mk cc46b93e96 fix: replace MediaRecorder with AudioWorklet (Blob URL) for voice capture 2026-05-26 12:13:43 +00:00
Mk d3e2ebca78 fix: replace ScriptProcessor with MediaRecorder for voice chat 2026-05-26 12:10:16 +00:00
Mk efcb5a0dd6 fix: client-authoritative mobs, spawn-only server, voice chat fix 2026-05-26 12:04:51 +00:00
12 changed files with 6592 additions and 447 deletions

View File

@ -4,6 +4,7 @@ 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 src/ /usr/share/nginx/html/src/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

128
build.js
View File

@ -1,128 +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 bundle.js > ../game-bundled.js
*/
const fs = require('fs');
const path = require('path');
const SRC = path.join(__dirname, 'src');
const ENTRY = 'main.js';
const visited = new Set();
const chunks = [];
// 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);
if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.js')) {
resolved += '.js';
}
return resolved;
}
function collectExports(content) {
const exports = [];
// export { a, b as c }
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);
}
// 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;
}
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] });
}
// 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] });
}
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);
// 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);
}
// 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+(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';
for (const chunk of chunks) {
output += `// === ${chunk.relPath} ===\n`;
output += chunk.content.trim();
output += '\n\n';
}
output += '})();\n';
process.stdout.write(output);

1931
game.js

File diff suppressed because it is too large Load Diff

4568
game.js.bak.v23 Normal file

File diff suppressed because it is too large Load Diff

97
index-new.html Normal file
View File

@ -0,0 +1,97 @@
<!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 (loaded as regular script, provides window.io) -->
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=5">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="saveBtn" class="rbtn pe">💾</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="resetBtn" class="rbtn pe">🔄</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:10px;top:120px;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>
<script type="module" src="src/main.js?v=9"></script>
</body>
</html>

97
index-old.html Normal file
View File

@ -0,0 +1,97 @@
<!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=5">
</head>
<body>
<div id="game">
<canvas id="c"></canvas>
<div class="ui">
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
<div class="row" id="multiplayerStatus" style="display:none;">👥 <span id="playerCount">0</span></div>
</div>
<div id="modeBtn" class="rbtn pe">⛏️</div>
<div id="saveBtn" class="rbtn pe">💾</div>
<div id="craftBtn" class="rbtn pe">🔨</div>
<div id="resetBtn" class="rbtn pe">🔄</div>
<div id="chatToggle" class="rbtn pe">💬</div>
<div id="invToggle" class="rbtn pe">📦</div>
<div id="mapToggle" class="rbtn pe">🗺️</div>
<div id="hotbar" class="pe"></div>
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:10px;top:120px;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>
<script src="game.js?v=8"></script>
</body>
</html>

View File

@ -6,7 +6,7 @@
<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=5">
<link rel="stylesheet" href="style.css?v=6">
</head>
<body>
@ -20,6 +20,7 @@
<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>
@ -35,7 +36,7 @@
</div>
<!-- Миникарта -->
<div id="minimapWrap" style="display:none;position:absolute;left:10px;top:120px;z-index:200;pointer-events:auto;">
<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>
@ -92,6 +93,6 @@
</div>
</div>
<script src="game.js?v=8"></script>
<script src="game.js?v=24"></script>
</body>
</html>

View File

@ -3,10 +3,15 @@ server {
root /usr/share/nginx/html;
index index.html;
location ~* \.(js|css)$ {
# CORS for ES modules
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";
add_header Access-Control-Allow-Origin *;
expires 0;
default_type application/javascript;
}
location / {

View File

@ -3,7 +3,7 @@ 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';

View File

@ -25,7 +25,7 @@ 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 './world/water.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';

View File

@ -91,3 +91,16 @@ body.touch-device #hotbar {
#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; }

70
voice-test.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html><head><title>Voice Test</title></head><body>
<h1>Voice Capture Test</h1>
<button id="btn" style="padding:20px;font-size:24px;background:#2ecc71;color:#fff;border:none;border-radius:12px;cursor:pointer;">Start Mic</button>
<div id="log" style="font-family:monospace;white-space:pre;margin-top:20px;"></div>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<script>
const log = document.getElementById('log');
function addLog(msg) { log.textContent += msg + '\n'; console.log(msg); }
let voiceStream, audioCtx, voiceProcessor, voiceSocket;
let debugCount = 0;
document.getElementById('btn').onclick = async () => {
try {
addLog('1. Requesting mic...');
voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true } });
addLog('2. Got stream: ' + voiceStream.getTracks().map(t => t.label + ' ' + t.readyState).join(', '));
audioCtx = new AudioContext({ sampleRate: 24000 });
if (audioCtx.state === 'suspended') await audioCtx.resume();
addLog('3. AudioContext: state=' + audioCtx.state + ' sampleRate=' + audioCtx.sampleRate);
const source = audioCtx.createMediaStreamSource(voiceStream);
voiceProcessor = audioCtx.createScriptProcessor(2048, 1, 1);
addLog('4. ScriptProcessor created');
voiceProcessor.onaudioprocess = (e) => {
debugCount++;
const pcm = e.inputBuffer.getChannelData(0);
const maxVal = Math.max(...Array.from(pcm).map(Math.abs));
if (debugCount <= 10) addLog('5. onaudioprocess #' + debugCount + ' samples=' + pcm.length + ' max=' + maxVal.toFixed(4));
if (!voiceSocket || !voiceSocket.connected) return;
const int16 = new Int16Array(pcm.length);
for (let i = 0; i < pcm.length; i++) {
const s = Math.max(-1, Math.min(1, pcm[i]));
int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
voiceSocket.emit('voice_data', int16.buffer);
};
const silentGain = audioCtx.createGain();
silentGain.gain.value = 0;
source.connect(voiceProcessor);
voiceProcessor.connect(silentGain);
silentGain.connect(audioCtx.destination);
addLog('6. Audio chain connected: source→processor→gain(0)→destination');
addLog('7. Connecting to voice server...');
voiceSocket = io('https://voicegrech.mkn8n.ru', { transports: ['websocket'] });
voiceSocket.on('connect', () => {
addLog('8. Socket connected: ' + voiceSocket.id);
voiceSocket.emit('voice_join', { world_id: 'test', x: 0, y: 0, name: 'Tester' });
addLog('9. Sent voice_join. Speak into mic — watch onaudioprocess logs above!');
});
voiceSocket.on('connect_error', (err) => addLog('ERROR: ' + err.message));
voiceSocket.on('voice_in', (payload) => {
addLog('VOICE_IN from ' + payload.meta.name + ' vol=' + payload.volume + ' bytes=' + payload.data.byteLength);
});
document.getElementById('btn').textContent = 'Listening...';
document.getElementById('btn').style.background = '#e74c3c';
} catch(e) {
addLog('ERROR: ' + e.message + '\n' + e.stack);
}
};
</script>
</body></html>