您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Полное исправление багов с картами и добавление обработки отмены действий
// ==UserScript== // @name Дурак онлайн Tracker // @namespace http://tampermonkey.net/ // @version 8.6 // @description Полное исправление багов с картами и добавление обработки отмены действий // @license MIT // @author lREDI // @match *https://durak.rstgames.com/play/* // @grant none // ==/UserScript== (function() { 'use strict'; const DECK_CONFIG = { 24: { start: 9, ranks: ['9','10','J','Q','K','A'], cols: 6 }, 36: { start: 6, ranks: ['6','7','8','9','10','J','Q','K','A'], cols: 9 }, 52: { start: 2, ranks: ['2','3','4','5','6','7','8','9','10','J','Q','K','A'], cols: 13 } }; const SUIT_ORDER = ['♠', '♥', '♣', '♦']; const RANK_POWER = { '2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8, '10': 9, 'J': 10, 'Q': 11, 'K': 12, 'A': 13 }; const CARD_SIZE = { width: 40, height: 60 }; const PLAYER_PANEL_WIDTH = 300; let gameState = { myId: null, deckType: null, deckSize: 0, trump: null, discardPile: new Set(), players: new Map(), activeCards: new Set(), currentAttack: [], currentDefense: [], cardOwners: new Map(), currentPlayer: null, fullDeck: [] }; let isUIAttached = false; let firstUpdate = true; let uiObserver = null; const generatePlayerColor = (() => { const colors = new Map(); return (id) => { if (!colors.has(id)) { const hue = Math.floor(Math.random() * 360); colors.set(id, `hsl(${hue}, 70%, 40%)`); } return colors.get(id); }; })(); function initUI() { if (isUIAttached) return; const container = document.createElement('div'); container.id = 'durak-tracker-container'; Object.assign(container.style, { position: 'fixed', right: '20px', top: '20px', display: 'flex', flexDirection: 'column', gap: '15px', zIndex: '99999', pointerEvents: 'none' }); const controlPanel = createControlPanel(); container.appendChild(controlPanel); container.appendChild(createDeckPanel()); container.appendChild(createPlayersPanel()); document.body.appendChild(container); isUIAttached = true; uiObserver = new MutationObserver((mutations) => { if (!document.body.contains(container)) { document.body.appendChild(container); } }); uiObserver.observe(document.body, { childList: true }); } function createControlPanel() { const panel = document.createElement('div'); panel.innerHTML = ` <button id="reset-tracker-btn" style=" padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; pointer-events: auto; ">🔄 Сбросить состояние</button> `; Object.assign(panel.style, { background: 'white', padding: '10px', borderRadius: '8px', boxShadow: '0 4px 10px rgba(0,0,0,0.15)', display: 'flex', justifyContent: 'center' }); panel.querySelector('#reset-tracker-btn').addEventListener('click', () => { resetGame(); showNotification('Состояние скрипта сброшено!'); }); return panel; } function showNotification(text, duration = 3000) { const notification = document.createElement('div'); Object.assign(notification.style, { position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)', background: '#4CAF50', color: 'white', padding: '12px 24px', borderRadius: '4px', boxShadow: '0 2px 6px rgba(0,0,0,0.2)', zIndex: '100000', animation: 'slideIn 0.3s ease-out', pointerEvents: 'none' }); notification.textContent = text; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease-in'; setTimeout(() => notification.remove(), 300); }, duration); } function createDeckPanel() { const panel = document.createElement('div'); panel.innerHTML = ` <h3 style="margin:0 0 10px 0;font-size:16px;pointer-events:auto;">🗑 Карты</h3> <div class="deck-grid" style=" display: flex; flex-direction: column; gap: 5px; max-height: 400px; overflow-y: auto; pointer-events:auto; "></div> `; Object.assign(panel.style, { background: 'white', padding: '15px', borderRadius: '8px', boxShadow: '0 4px 10px rgba(0,0,0,0.15)' }); return panel; } function createPlayersPanel() { const panel = document.createElement('div'); panel.innerHTML = ` <h3 style="margin:0 0 10px 0;font-size:16px;pointer-events:auto;">🎮 Игроки</h3> <div id="players-content" style=" max-height: 500px; overflow-y: auto; pointer-events:auto; "></div> `; Object.assign(panel.style, { width: `${PLAYER_PANEL_WIDTH}px`, background: 'white', padding: '15px', borderRadius: '8px', boxShadow: '0 4px 10px rgba(0,0,0,0.15)' }); return panel; } function parseCard(cardStr) { const patterns = [ /^([♦♣♥♠])(10|[2-9JQKA])$/, /^(10|[2-9JQKA])([♦♣♥♠])$/ ]; for (const pattern of patterns) { const match = cardStr.match(pattern); if (match) { const suit = match[1]?.match(/[♦♣♥♠]/)?.[0] || match[3]; const value = match[2] || match[1]; return { value: value, suit: suit, id: `${value}${suit}` }; } } console.error('[CARD] Неверный формат:', cardStr); return { value: '?', suit: '?', id: 'invalid' }; } function generateFullDeck() { const sortedDeck = []; const config = DECK_CONFIG[gameState.deckType]; SUIT_ORDER.forEach(suit => { config.ranks .sort((a, b) => RANK_POWER[a] - RANK_POWER[b]) .forEach(rank => { sortedDeck.push({ value: rank, suit: suit, id: `${rank}${suit}`, isTrump: suit === gameState.trump }); }); }); return sortedDeck; } function updateDeckView() { const grid = document.querySelector('.deck-grid'); if (!grid) return; if (!gameState.deckType || !DECK_CONFIG[gameState.deckType]) { grid.innerHTML = '<div style="color:red; padding:10px;">Ошибка загрузки колоды</div>'; return; } const suitsGroup = {'♠': [], '♥': [], '♣': [], '♦': []}; gameState.fullDeck.forEach(card => { suitsGroup[card.suit].push(card); }); grid.innerHTML = ''; SUIT_ORDER.forEach(suit => { const group = document.createElement('div'); Object.assign(group.style, { display: 'flex', gap: '3px', marginBottom: '5px', flexWrap: 'wrap' }); suitsGroup[suit].forEach(card => { const isActive = gameState.activeCards.has(card.id); const isDiscarded = gameState.discardPile.has(card.id); const ownerId = gameState.cardOwners.get(card.id); const cardEl = document.createElement('div'); Object.assign(cardEl.style, { width: `${CARD_SIZE.width}px`, height: `${CARD_SIZE.height}px`, border: `1px solid ${card.isTrump ? '#ff5722' : '#ddd'}`, borderRadius: '4px', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', position: 'relative', fontSize: '12px', background: getCardBackground(card), color: getCardColor(card), pointerEvents: 'none' }); if (isActive) { cardEl.style.boxShadow = '0 0 0 2px #ffeb3b'; cardEl.style.animation = 'pulse 1.5s infinite'; } cardEl.innerHTML = ` <div class="card-value">${card.value}</div> <div class="card-suit">${card.suit}</div> ${card.isTrump ? '<div style="position:absolute;top:2px;right:2px;color:#ff9800;font-size:10px;">★</div>' : ''} ${isDiscarded ? '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#f44336;font-size:24px;opacity:0.8;">✖</div>' : ''} `; cardEl.dataset.cardId = card.id; group.appendChild(cardEl); }); grid.appendChild(group); }); } function getCardColor(card) { if (gameState.discardPile.has(card.id)) return '#fff'; if (gameState.cardOwners.has(card.id)) return '#fff'; return ['♦','♥'].includes(card.suit) ? '#c62828' : '#2d2d2d'; } function getCardBackground(card) { if (gameState.discardPile.has(card.id)) return '#616161'; if (gameState.cardOwners.has(card.id)) { return generatePlayerColor(gameState.cardOwners.get(card.id)); } return 'white'; } function handleGameMessage(type, data) { console.log('[WS IN] Processing:', type, data); try { switch(type) { case 'game': initDeck(data); gameState.currentPlayer = data.currentPlayer; if(data.players?.[0]?.id && !gameState.myId) { gameState.myId = data.players[0].id; } setTimeout(() => updateUI(), 500); break; case 't': case 'f': case 's': processAttack(data.c); break; case 'b': processDefense(data.c, data.b); break; case 'rt': // Отмена атаки case 'rf': // Отмена атаки case 'rs': // Отмена атаки revertAttack(data.c); break; case 'rb': // Отмена защиты revertDefense(data.c, data.b); break; case 'end_turn': processEndTurn(data || {}); break; case 'mode': handlePlayerModes(data); break; case 'game_reset': resetGame(); break; case 'cp': if(!gameState.myId && data.user) { gameState.myId = data.id; } updatePlayer(data); break; case 'hand': if(gameState.myId !== null) { Array.from(gameState.cardOwners.entries()) .filter(([card, owner]) => owner === gameState.myId) .forEach(([card]) => gameState.cardOwners.delete(card)); data.cards.forEach(cardStr => { const card = parseCard(cardStr); if(card && card.id !== 'invalid') { gameState.cardOwners.set(card.id, gameState.myId); } }); } break; case 'p': updatePlayer(data); break; } updateUI(); } catch (e) { console.error('[INCOMING ERROR]', e); } } function handleOutgoingMessage(type, data) { console.log('[WS OUT] Processing:', type, data); try { switch(type) { case 't': case 'f': case 's': if(data.c) { const cards = Array.isArray(data.c) ? data.c : [data.c]; cards.forEach(card => processAttack(card, true)); } break; case 'b': if(data.c && data.b) { processDefense(data.c, data.b, true); } break; case 'take': handleTakeCards(gameState.currentPlayer); break; } updateUI(); } catch(e) { console.error('[OUTGOING ERROR]', e); } } function initDeck(data) { const deckSize = data.deck; if ([24, 36, 52].includes(deckSize)) { gameState.deckType = deckSize; } else { console.error('[ERROR] Invalid deck size:', deckSize); gameState.deckType = 36; } gameState.fullDeck = generateFullDeck(); gameState.trump = data.trump?.[0] || '?'; gameState.deckSize = deckSize; } function processAttack(cardId, isOutgoing = false) { const card = parseCard(cardId); if (!card || card.id === 'invalid') return; if (!gameState.discardPile.has(card.id)) { gameState.currentAttack.push(card.id); gameState.activeCards.add(card.id); // Не удаляем карту из cardOwners, так как она может быть отменена if (isOutgoing && gameState.myId !== null) { // Помечаем карту как активную, но не удаляем из владельца gameState.cardOwners.set(card.id, gameState.myId); } } } function processDefense(attackCard, defenseCard, isOutgoing = false) { const attack = parseCard(attackCard); const defense = parseCard(defenseCard); if (!attack || !defense) return; const isValid = ![attack.id, defense.id].some(c => gameState.discardPile.has(c) || (gameState.cardOwners.has(c) && gameState.cardOwners.get(c) !== gameState.myId) ); if (isValid) { gameState.currentDefense.push({ attack: attack.id, defense: defense.id }); [attack.id, defense.id].forEach(c => { gameState.activeCards.add(c); // Не удаляем карту из cardOwners, так как она может быть отменена if (isOutgoing && c === defense.id && gameState.myId !== null) { // Помечаем карту как активную, но не удаляем из владельца gameState.cardOwners.set(c, gameState.myId); } }); } } function revertAttack(cardId) { const card = parseCard(cardId); if (!card || card.id === 'invalid') return; // Удаляем карту из активных gameState.activeCards.delete(card.id); // Возвращаем карту владельцу (если она была на руках) if (gameState.cardOwners.has(card.id)) { const ownerId = gameState.cardOwners.get(card.id); gameState.players.get(ownerId)?.cards.push(card.id); } // Удаляем карту из текущей атаки gameState.currentAttack = gameState.currentAttack.filter(id => id !== card.id); } function revertDefense(attackCard, defenseCard) { const attack = parseCard(attackCard); const defense = parseCard(defenseCard); if (!attack || !defense) return; // Удаляем карты из активных gameState.activeCards.delete(attack.id); gameState.activeCards.delete(defense.id); // Возвращаем карты владельцам (если они были на руках) if (gameState.cardOwners.has(attack.id)) { const ownerId = gameState.cardOwners.get(attack.id); gameState.players.get(ownerId)?.cards.push(attack.id); } if (gameState.cardOwners.has(defense.id)) { const ownerId = gameState.cardOwners.get(defense.id); gameState.players.get(ownerId)?.cards.push(defense.id); } // Удаляем карты из текущей защиты gameState.currentDefense = gameState.currentDefense.filter( pair => pair.attack !== attack.id && pair.defense !== defense.id ); } function removeFromOwners(cardId) { gameState.cardOwners.delete(cardId); } function processEndTurn(data) { console.log('[END TURN] Data:', data); const allCards = [ ...gameState.activeCards, ...gameState.currentAttack, ...gameState.currentDefense.flatMap(p => [p.attack, p.defense]) ]; const uniqueCards = [...new Set(allCards)]; uniqueCards.forEach(c => { gameState.activeCards.delete(c); if(data.id !== undefined) { gameState.cardOwners.set(c, data.id); gameState.discardPile.delete(c); } else { gameState.discardPile.add(c); gameState.cardOwners.delete(c); } }); gameState.currentAttack = []; gameState.currentDefense = []; if(data.id !== undefined) { gameState.activeCards.forEach(c => { if(gameState.cardOwners.get(c) === data.id) { gameState.activeCards.delete(c); } }); } updateUI(); } function handlePlayerModes(data) { Object.entries(data).forEach(([playerId, mode]) => { const pid = Number(playerId); const player = gameState.players.get(pid); if (player) { player.mode = mode; if (mode === 7) handleTakeCards(pid); } }); } function handleTakeCards(playerId) { const taken = Array.from(gameState.activeCards) .filter(c => !gameState.discardPile.has(c)); taken.forEach(c => { gameState.cardOwners.set(c, playerId); gameState.activeCards.delete(c); }); gameState.currentAttack = []; gameState.currentDefense = []; } function updatePlayer(data) { const player = { id: data.id, name: data.user?.name || 'Аноним', avatar: data.user?.avatar || 'https://via.placeholder.com/30', cards: data.user?.cards || [], mode: data.mode || 6, isActive: data.id === gameState.currentPlayer }; gameState.players.set(data.id, player); } function updateUI() { if (firstUpdate) { setTimeout(() => { updateDeckView(); updatePlayersUI(); firstUpdate = false; }, 1000); } else { updateDeckView(); updatePlayersUI(); } } function updatePlayersUI() { const content = document.getElementById('players-content'); if (!content) return; content.innerHTML = Array.from(gameState.players.values()).map(player => ` <div class="player" style=" margin:8px 0; padding:12px; background:${player.isActive ? '#f8f9fa' : '#fff'}; border:2px solid ${player.isActive ? generatePlayerColor(player.id) : '#eee'}; border-radius:8px; "> <div style="display:flex;align-items:center;gap:10px;"> <img src="${player.avatar}" style="width:32px;height:32px;border-radius:50%;object-fit:cover;"> <div style="flex-grow:1;"> <div style="font-weight:600;color:${generatePlayerColor(player.id)}"> ${player.name} </div> <div style="font-size:12px;color:#666;"> <span class="status ${getStatusClass(player.mode)}"> ${getStatusText(player.mode)} </span> • ${player.cards.length} 🃏 </div> </div> </div> ${renderTakenCards(player.id)} </div> `).join(''); } function renderTakenCards(playerId) { const taken = Array.from(gameState.cardOwners.entries()) .filter(([cardId, owner]) => owner === playerId && !gameState.discardPile.has(cardId)) .map(([card]) => card); return taken.length ? ` <div style="margin-top:8px;padding-top:8px;border-top:1px dashed #eee;font-size:12px;color:#666;"> Взято: ${taken.slice(-3).join(', ')}${taken.length > 3 ? ` (+${taken.length - 3})` : ''} </div> ` : ''; } function getStatusClass(mode) { const map = {0:'status-throw',1:'status-attack',7:'status-take',8:'status-defend',9:'status-defend'}; return map[mode] || 'status-wait'; } function getStatusText(mode) { const map = {0:'Подкидывает',1:'Атакует',7:'Взял',8:'Защита',9:'Отбивается'}; return map[mode] || 'Ожидает'; } function resetGame() { gameState = { myId: null, deckType: null, deckSize: 0, trump: null, discardPile: new Set(), players: new Map(), activeCards: new Set(), currentAttack: [], currentDefense: [], cardOwners: new Map(), currentPlayer: null, fullDeck: [] }; updateUI(); showNotification('Состояние скрипта сброшено!'); } const css = ` @keyframes pulse { 0% { box-shadow:0 0 0 0 rgba(255,235,59,0.4); } 70% { box-shadow:0 0 0 6px rgba(255,235,59,0); } 100% { box-shadow:0 0 0 0 rgba(255,235,59,0); } } @keyframes slideIn { from { transform: translate(-50%, -30px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } } @keyframes slideOut { from { transform: translate(-50%, 0); opacity: 1; } to { transform: translate(-50%, -30px); opacity: 0; } } .status-throw { background:#d1ecf1; color:#0c5460; } .status-attack { background:#fff3cd; color:#856404; } .status-defend { background:#d4edda; color:#155724; } .status-take { background:#f8d7da; color:#721c24; } .status-wait { background:#e9ecef; color:#495057; } #reset-tracker-btn:hover { background: #c82333 !important; } .deck-grid::-webkit-scrollbar, #players-content::-webkit-scrollbar { width:6px; } .deck-grid::-webkit-scrollbar-track, #players-content::-webkit-scrollbar-track { background:#f1f1f1; } .deck-grid::-webkit-scrollbar-thumb, #players-content::-webkit-scrollbar-thumb { background:#888; border-radius:3px; } `; function init() { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); initUI(); const OriginalWebSocket = window.WebSocket; window.WebSocket = function(url) { const ws = new OriginalWebSocket(url); const originalSend = ws.send.bind(ws); ws.send = function(data) { try { const strData = String(data); const typeEnd = strData.indexOf('{'); const type = typeEnd > 0 ? strData.slice(0, typeEnd) : strData; const jsonData = typeEnd > 0 ? JSON.parse(strData.slice(typeEnd)) : {}; handleOutgoingMessage(type, jsonData); } catch(e) { console.error('[WS SEND ERROR]', e); } originalSend(data); }; ws.addEventListener('message', event => { try { const raw = event.data.toString(); const typeEnd = raw.indexOf('{'); const type = typeEnd > 0 ? raw.slice(0, typeEnd) : raw; const data = typeEnd > 0 ? JSON.parse(raw.slice(typeEnd)) : {}; handleGameMessage(type, data); } catch(e) { console.error('[WS RECV ERROR]', e); } }); return ws; }; } if (document.readyState === 'complete') init(); else window.addEventListener('load', init); })();