diff --git a/game.js b/game.js
index 1a7ea26..bab6ed4 100644
--- a/game.js
+++ b/game.js
@@ -223,6 +223,89 @@ function customConfirm(msg, onYes) {
Telegram.WebApp.switchInlineQuery('play ' + code);
};
box.appendChild(btnInvite);
+
+ // Friends button (TG only)
+ const btnFriends = document.createElement('button');
+ btnFriends.style.cssText = 'width:100%;padding:14px;margin-bottom:12px;background:#f39c12;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;';
+ btnFriends.textContent = '👥 Друзья';
+ btnFriends.onmouseover = () => btnFriends.style.background = '#e67e22';
+ btnFriends.onmouseout = () => btnFriends.style.background = '#f39c12';
+ btnFriends.onclick = async () => {
+ try {
+ const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
+ const data = await resp.json();
+ if (!data.ok || !data.friends || !data.friends.length) {
+ showToast('👥 Нет друзей. Поделись своим TG ID: ' + tgUser.id);
+ return;
+ }
+ let html = '
';
+ for (const f of data.friends) {
+ if (f.online) {
+ html += '
🟢 ' + f.name + ' — мир ' + f.world_id + '
';
+ } else {
+ html += '
⚫ ' + f.name + '
';
+ }
+ }
+ html += '
';
+ box.innerHTML = '';
+ const ft = document.createElement('div');
+ ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;';
+ ft.textContent = '👥 Друзья';
+ box.appendChild(ft);
+ const list = document.createElement('div');
+ list.innerHTML = html;
+ box.appendChild(list);
+ // Add friend by TG ID
+ const addDiv = document.createElement('div');
+ addDiv.style.cssText = 'margin-top:16px;';
+ addDiv.innerHTML = 'Добавить друга по TG ID:
';
+ const addInput = document.createElement('input');
+ addInput.type = 'number';
+ addInput.placeholder = 'TG ID друга';
+ addInput.style.cssText = 'width:70%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
+ const addBtn = document.createElement('button');
+ addBtn.style.cssText = 'width:25%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;';
+ addBtn.textContent = '+';
+ addBtn.onclick = async () => {
+ const fid = parseInt(addInput.value);
+ if (!fid) return;
+ const r = await fetch(SERVER_URL + '/api/tg/friends/add', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,friend_id:fid})});
+ const d = await r.json();
+ if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.click(); }
+ else { showToast('❌ Ошибка'); }
+ };
+ addDiv.appendChild(addInput);
+ addDiv.appendChild(addBtn);
+ box.appendChild(addDiv);
+ // My TG ID
+ const myId = document.createElement('div');
+ myId.style.cssText = 'margin-top:12px;color:#888;font-size:13px;';
+ myId.textContent = '🆔 Твой ID: ' + tgUser.id + ' — отправь другу, чтобы он добавил тебя';
+ box.appendChild(myId);
+ // Join online friend buttons
+ const onlineFriends = (data.friends||[]).filter(f => f.online);
+ if (onlineFriends.length) {
+ for (const of2 of onlineFriends) {
+ const joinBtn = document.createElement('button');
+ joinBtn.style.cssText = 'width:100%;padding:12px;margin-top:8px;background:#27ae60;color:#fff;border:none;border-radius:10px;font-size:16px;cursor:pointer;';
+ joinBtn.textContent = '🎮 Вступить к ' + of2.name + ' (мир ' + of2.world_id + ')';
+ joinBtn.onclick = () => {
+ worldId = of2.world_id;
+ overlay.remove();
+ resolve(worldId);
+ };
+ box.appendChild(joinBtn);
+ }
+ }
+ // Back button
+ const btnBackF = document.createElement('button');
+ btnBackF.style.cssText = 'width:100%;padding:10px;margin-top:12px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;';
+ btnBackF.textContent = '← Назад';
+ btnBackF.onclick = () => { overlay.remove(); showStartMenu().then(resolve); };
+ box.appendChild(btnBackF);
+ } catch(e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); }
+ };
+ box.appendChild(btnFriends);
}
// Player name input (if not set)
@@ -379,6 +462,14 @@ function customConfirm(msg, onYes) {
// Присоединяемся к миру
socket.emit('join_world', { world_id: worldId, player_name: playerName, tg_id: (tgUser && tgUser.id) ? tgUser.id : null });
+ // TG: отправляем tg_auth + онлайн статус для уведомления друзей
+ if (tgUser && tgUser.id) {
+ socket.emit('tg_auth', { tg_id: tgUser.id });
+ try {
+ fetch(SERVER_URL + '/api/tg/online', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,world_id:worldId,player_name:playerName||'Игрок'})});
+ } catch(e) { console.warn('[TG] online notify error:', e); }
+ }
+
// Показываем в UI
worldIdEl.textContent = worldId;
// XP/Level display
@@ -401,6 +492,10 @@ function customConfirm(msg, onYes) {
isMultiplayer = false;
otherPlayers.clear();
multiplayerStatus.style.display = 'none';
+ // TG: убираем онлайн статус
+ if (tgUser && tgUser.id) {
+ try { fetch(SERVER_URL + '/api/tg/offline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id})}); } catch(e) {}
+ }
});
// Обработка world_state
@@ -506,6 +601,12 @@ function customConfirm(msg, onYes) {
serverMobs.clear();
for (const m of data.mobs) {
const sm = createMobFromServer(m);
+ // Пересчитываем Y по клиентской surfaceGyAt
+ const spawnGX = Math.floor(sm.x / TILE);
+ genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1);
+ const clientSurfaceY = surfaceGyAt(spawnGX);
+ sm.y = (clientSurfaceY - 1) * TILE;
+ sm.vy = 0;
serverMobs.set(m.id, sm);
}
}
@@ -525,6 +626,8 @@ function customConfirm(msg, onYes) {
otherPlayers.set(data.socket_id, {
x: safeSpawnX,
y: safeSpawnY,
+ tx: safeSpawnX,
+ ty: safeSpawnY,
color: getRandomPlayerColor(data.socket_id),
name: data.player_name || 'Игрок'
});
@@ -538,8 +641,9 @@ function customConfirm(msg, onYes) {
socket.on('player_moved', (data) => {
if (data.socket_id !== mySocketId && otherPlayers.has(data.socket_id)) {
const p = otherPlayers.get(data.socket_id);
- p.x = data.x;
- p.y = data.y;
+ // Lerp: запоминаем целевую позицию, рисуем в рендере с интерполяцией
+ p.tx = data.x;
+ p.ty = data.y;
// Обновляем имя, если оно пришло
if (data.player_name) {
p.name = data.player_name;
@@ -560,6 +664,14 @@ function customConfirm(msg, onYes) {
socket.on('mob_spawned', (data) => {
const sm = createMobFromServer(data);
+ // Пересчитываем Y по клиентской surfaceGyAt — сервер может ошибаться
+ const spawnGX = Math.floor(sm.x / TILE);
+ // Убеждаемся что чанк сгенерирован (иначе getBlock=null → моб падает сквозь землю)
+ genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1);
+ const clientSurfaceY = surfaceGyAt(spawnGX);
+ sm.y = (clientSurfaceY - 1) * TILE; // 1 блок выше поверхности (под ногами)
+ sm.vy = 0;
+ sm.grounded = false; // resolveY в mobAI поставит grounded=true
serverMobs.set(data.id, sm);
});
@@ -571,8 +683,8 @@ function customConfirm(msg, onYes) {
hp: u.hp || 1, maxHp: u.hp || 1, dir: u.dir || 1 });
serverMobs.set(u.id, sm);
}
- sm.x += (u.x - sm.x) * 0.3;
- sm.y += (u.y - sm.y) * 0.3;
+ sm.x += (u.x - sm.x) * 0.35;
+ sm.y += (u.y - sm.y) * 0.35;
sm.dir = u.dir != null ? u.dir : sm.dir;
sm.hp = u.hp != null ? u.hp : sm.hp;
sm.fuse = u.fuse != null ? u.fuse : sm.fuse;
@@ -1414,6 +1526,31 @@ function customConfirm(msg, onYes) {
const syEl = document.getElementById('sy');
const todEl = document.getElementById('tod');
const worldIdEl = document.getElementById('worldId');
+ // Toast уведомление
+ function showToast(msg, duration) {
+ duration = duration || 1800;
+ const t = document.createElement('div');
+ t.textContent = msg;
+ t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;';
+ document.body.appendChild(t);
+ setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration);
+ }
+ // Клик на worldId → копировать код мира + toast
+ worldIdEl.addEventListener('click', () => {
+ const code = worldId || '';
+ if (!code) return;
+ const text = code; // копируем только код, не URL
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => showToast('📋 Скопировано: ' + code)).catch(() => showToast(code));
+ } else {
+ // fallback
+ const ta = document.createElement('textarea');
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
+ document.body.appendChild(ta); ta.select();
+ try { document.execCommand('copy'); showToast('📋 Скопировано: ' + code); } catch(e) { showToast(code); }
+ ta.remove();
+ }
+ });
const playerCountEl = document.getElementById('playerCount');
const hotbarEl = document.getElementById('hotbar');
const craftPanel = document.getElementById('craftPanel');
@@ -4836,7 +4973,7 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
genColumn(gx);
const sgy = surfaceGyAt(gx);
const wx = gx*TILE + 4;
- const wy = (sgy-2)*TILE;
+ const wy = (sgy-1)*TILE; // 1 блок выше поверхности
// не спавнить в воде
const top = getBlock(gx, sgy);
@@ -4874,16 +5011,15 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
mobAI(m, dt);
if(m.hp<=0) mobs.splice(i,1);
}
- // Server-spawned mobs (MP client-authoritative)
+ // Server-spawned mobs: server is authoritative for positions
+ // mobAI does NOT run on serverMobs — server handles all physics + AI
+ // Only remove dead mobs from local map
if(isMultiplayer){
for (const [id, sm] of serverMobs) {
- mobAI(sm, dt);
if(sm.hp <= 0){
- // Schedule removal (don't delete during iteration)
sm.dead = true;
}
}
- // Remove dead server mobs
for (const [id, sm] of serverMobs) {
if(sm.dead) serverMobs.delete(id);
}
@@ -5097,6 +5233,13 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
ctx.drawImage(tex['boat'], boat.x - (TILE-boat.w)/2, boat.y - (TILE-boat.h)/2, TILE, TILE);
}
+ // Lerp other players towards target positions (smooth movement)
+ for(const [socketId, p] of otherPlayers){
+ if (p.tx !== undefined) {
+ p.x += (p.tx - p.x) * 0.25;
+ p.y += (p.ty - p.y) * 0.25;
+ }
+ }
// other players (multiplayer)
for(const [socketId, p] of otherPlayers){
if(heroImg.complete){
diff --git a/index.html b/index.html
index ca0b9fc..09875a8 100644
--- a/index.html
+++ b/index.html
@@ -95,6 +95,6 @@
-
+