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
This commit is contained in:
parent
528a698cf0
commit
3733b4b94f
161
game.js
161
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 = '<div style="text-align:left;margin-bottom:16px;">';
|
||||
for (const f of data.friends) {
|
||||
if (f.online) {
|
||||
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">🟢 <b>' + f.name + '</b> — мир ' + f.world_id + '</div>';
|
||||
} else {
|
||||
html += '<div style="padding:8px 0;border-bottom:1px solid #333;">⚫ ' + f.name + '</div>';
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
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 = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по TG ID:</div>';
|
||||
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){
|
||||
|
|
|
|||
|
|
@ -95,6 +95,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="game.js?v=45"></script>
|
||||
<script src="game.js?v=48"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue