Compare commits

...

9 Commits

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

766
game.js

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,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://telegram.org/js/telegram-web-app.js"></script>
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
<link rel="stylesheet" href="style.css?v=6">
</head>
@ -17,6 +18,7 @@
<div id="stats">
<div class="row">❤️ <span id="hp">100</span> &nbsp; 🍗 <span id="food">100</span></div>
<div class="row">🫁 <span id="o2">100</span></div>
<div class="row">🌡️ <span id="temp">15°C</span></div>
<div class="row">📍 X:<span id="sx">0</span> Y:<span id="sy">0</span></div>
<div class="row">🕒 <span id="tod">День</span></div>
<div class="row">🌐 <span id="worldId" style="cursor:pointer; text-decoration:underline;" title="Нажмите, чтобы скопировать ссылку">default</span></div>
@ -93,6 +95,6 @@
</div>
</div>
<script src="game.js?v=24"></script>
<script src="game.js?v=50"></script>
</body>
</html>

View File

@ -3,7 +3,6 @@ server {
root /usr/share/nginx/html;
index index.html;
# CORS for ES modules
add_header Access-Control-Allow-Origin *;
location ~* \.(js|mjs|css)$ {
@ -17,4 +16,4 @@ server {
location / {
try_files $uri $uri/ /index.html;
}
}
}

View File

@ -1,70 +1,94 @@
<!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>
<html>
<head><title>Voice Test</title></head>
<body style="background:#1a1a2e;color:#eee;font-family:monospace;padding:20px">
<h2>Voice Chat Debug</h2>
<div id="log" style="white-space:pre;overflow:auto;max-height:80vh;border:1px solid #444;padding:10px;font-size:12px"></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); }
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
const logEl = document.getElementById('log');
function log(msg) { logEl.textContent += new Date().toISOString().substr(11,8) + ' ' + msg + '\n'; logEl.scrollTop = logEl.scrollHeight; }
let voiceStream, audioCtx, voiceProcessor, voiceSocket;
let debugCount = 0;
log('Starting voice test...');
document.getElementById('btn').onclick = async () => {
(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));
log('Requesting microphone...');
const stream = await navigator.mediaDevices.getUserMedia({audio: {echoCancellation:true,noiseSuppression:true,autoGainControl:true}});
log('Got mic stream: ' + stream.getTracks().map(t => t.label + ' ' + t.readyState).join(', '));
const audioCtx = new AudioContext({sampleRate: 16000});
log('AudioContext: state=' + audioCtx.state + ' sampleRate=' + audioCtx.sampleRate);
if (audioCtx.state === 'suspended') { await audioCtx.resume(); log('AudioContext resumed: ' + audioCtx.state); }
const source = audioCtx.createMediaStreamSource(stream);
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
source.connect(analyser);
// Check mic levels
const dataArr = new Float32Array(analyser.fftSize);
let checkCount = 0;
const checkInterval = setInterval(() => {
analyser.getFloatTimeDomainData(dataArr);
let sum = 0;
for (let i = 0; i < dataArr.length; i++) sum += dataArr[i] * dataArr[i];
const rms = Math.sqrt(sum / dataArr.length);
checkCount++;
if (checkCount <= 20 || checkCount % 50 === 0) log('Mic RMS: ' + rms.toFixed(6) + (rms > 0.008 ? ' SPEAKING' : ''));
}, 200);
log('Connecting to voice server...');
const socket = io(VOICE_SERVER, {transports: ['websocket']});
socket.on('connect', () => {
log('SOCKET CONNECTED: ' + socket.id);
socket.emit('voice_join', {world_id: 'test', x: 0, y: 0, name: 'Tester', mode: 'world', codec: 'pcm'});
log('voice_join sent');
});
socket.on('connect_error', (e) => {
log('SOCKET ERROR: ' + e.message);
});
socket.on('voice_in', (payload) => {
log('RX voice_in from ' + (payload.meta?.from||'?').substring(0,8) + ' codec:' + payload.codec + ' size:' + (payload.data?.byteLength || payload.data?.length || '?'));
});
// Capture and send
const processor = audioCtx.createScriptProcessor(320, 1, 1);
source.connect(processor);
processor.connect(audioCtx.destination);
let seq = 0;
let sendCount = 0;
processor.onaudioprocess = (e) => {
if (!socket.connected) return;
const float32 = e.inputBuffer.getChannelData(0);
let rms = 0;
for (let i = 0; i < float32.length; i++) rms += float32[i] * float32[i];
rms = Math.sqrt(rms / float32.length);
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]));
if (rms < 0.005) return; // Skip silence
const int16 = new Int16Array(float32.length);
for (let i = 0; i < float32.length; i++) {
const s = Math.max(-1, Math.min(1, float32[i]));
int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
voiceSocket.emit('voice_data', int16.buffer);
socket.emit('voice_data', {codec: 'pcm', data: int16.buffer, seq: seq++, speaking: true});
sendCount++;
if (sendCount <= 5 || sendCount % 50 === 0) log('TX frame #' + sendCount + ' rms:' + rms.toFixed(4) + ' size:' + int16.buffer.byteLength);
};
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';
log('Voice capture active. Speak into mic!');
} catch(e) {
addLog('ERROR: ' + e.message + '\n' + e.stack);
log('ERROR: ' + e.message);
}
};
})();
</script>
</body></html>
</body>
</html>