277 lines
11 KiB
JavaScript
277 lines
11 KiB
JavaScript
// 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}...`);
|
||
} |