grechka-game/src3d/ui/trade.js

277 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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}...`);
}