BW WAR Tracker (REST API v2.0)

Track targets, sync assists, faction import, advanced stats formatting

目前為 2025-11-25 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         BW WAR Tracker (REST API v2.0)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Track targets, sync assists, faction import, advanced stats formatting
// @author       Tu
// @match        https://www.torn.com/*
// @icon         https://www.torn.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @connect      www.tornstats.com
// @connect      bw-war-tracker-default-rtdb.europe-west1.firebasedatabase.app
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    let currentUser = null;
    let factionMembers = [];
    // myTargets is now synced via Firebase 'targets' node, local storage removed for sync
    let targetsList = []; 
    let assists = [];
    let isPolling = false;
    
    // --- CONFIGURARE ---
    const ADMIN_ID = 3946434; 
    const DB_URL = "https://bw-war-tracker-default-rtdb.europe-west1.firebasedatabase.app";

    function initScript() {
        if (document.body) {
            setupScript();
        } else {
            setTimeout(initScript, 100);
        }
    }

    function setupScript() {
        GM_addStyle(`
        /* UI GENERAL - MONOCHROME THEME */
        #bw-war-button { position: fixed !important; bottom: 20px !important; left: 20px !important; background-color: #000 !important; color: #fff !important; border: 2px solid #fff !important; padding: 12px 24px !important; font-size: 14px !important; font-weight: bold; cursor: pointer; border-radius: 5px; z-index: 999999 !important; box-shadow: 0 2px 10px rgba(0,0,0,0.3) !important; transition: all 0.3s !important; }
        #bw-war-button:hover { background-color: #333 !important; transform: translateY(-2px) !important; }
        
        #bw-war-modal, #bw-settings-modal { display: none !important; position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; width: 85% !important; max-width: 1000px !important; height: 75% !important; background-color: rgba(26, 26, 26, 0.98) !important; border: 2px solid #333 !important; border-radius: 10px !important; z-index: 999999 !important; box-shadow: 0 5px 30px rgba(0,0,0,0.8) !important; }
        #bw-war-modal.active, #bw-settings-modal.active { display: flex !important; flex-direction: column !important; overflow: hidden !important; }
        
        #bw-war-overlay { display: none !important; position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background-color: rgba(0,0,0,0.8) !important; z-index: 999998 !important; }
        #bw-war-overlay.active { display: block !important; }
        
        /* HEADER & BUTTONS */
        .bw-modal-header { background: #000 !important; color: #fff !important; padding: 15px !important; border-radius: 8px 8px 0 0 !important; display: flex !important; flex-direction: column !important; align-items: center !important; border-bottom: 2px solid #333 !important; position: relative !important; }
        .bw-modal-title { font-size: 22px !important; font-weight: 900 !important; letter-spacing: 1px; }
        
        .bw-header-buttons { position: absolute !important; top: 15px !important; right: 15px !important; display: flex !important; gap: 8px !important; }
        .bw-settings-button, .bw-close-button { background: none !important; border: 1px solid #fff !important; color: #fff !important; font-size: 16px !important; cursor: pointer !important; padding: 5px !important; border-radius: 4px !important; width: 30px !important; height: 30px !important; display: flex !important; justify-content: center !important; align-items: center !important; }
        .bw-settings-button:hover, .bw-close-button:hover { background: #333 !important; }

        /* TABS */
        .bw-tabs { display: flex !important; background-color: #1a1a1a !important; border-bottom: 1px solid #333 !important; }
        .bw-tab { flex: 1 !important; padding: 15px !important; background-color: #1a1a1a !important; color: #888 !important; border: none !important; cursor: pointer !important; font-weight: bold !important; transition: 0.2s; }
        .bw-tab.active { background-color: #000 !important; color: #fff !important; border-bottom: 3px solid #fff !important; }
        .bw-tab-content { display: none !important; padding: 20px !important; overflow-y: auto !important; flex: 1 !important; color: #fff !important; }
        .bw-tab-content.active { display: block !important; }

        /* LIST ITEMS */
        .bw-member-item, .bw-target-item, .bw-assist-item { background-color: #111 !important; border: 1px solid #333 !important; padding: 12px 15px !important; border-radius: 5px !important; margin-bottom: 8px !important; display: flex !important; justify-content: space-between !important; align-items: center !important; }
        .bw-member-name, .bw-target-name { color: #fff !important; cursor: pointer !important; font-weight: bold !important; text-decoration: none !important; font-size: 14px !important; }
        .bw-member-name:hover, .bw-target-name:hover { color: #ccc !important; text-decoration: underline !important; }
        
        .bw-rank-badge { background-color: #fff !important; color: #000 !important; padding: 2px 8px !important; border-radius: 10px !important; font-size: 10px !important; font-weight: bold !important; text-transform: uppercase; }

        /* INPUTS & BUTTONS */
        .bw-input-section { display: flex !important; gap: 10px !important; margin-bottom: 20px !important; }
        .bw-input-field { flex: 1 !important; padding: 10px !important; background-color: #000 !important; border: 1px solid #444 !important; color: #fff !important; border-radius: 4px !important; }
        
        /* Standard Buttons (Add/Attack) */
        .bw-add-button { background-color: #000 !important; color: #fff !important; border: 1px solid #fff !important; padding: 8px 20px !important; border-radius: 4px !important; cursor: pointer !important; font-weight: bold !important; transition: 0.2s; }
        .bw-add-button:hover { background-color: #fff !important; color: #000 !important; }
        
        /* Remove Button */
        .bw-remove-button { background-color: #000 !important; color: #fff !important; border: 1px solid #444 !important; padding: 5px 12px !important; border-radius: 3px !important; cursor: pointer !important; font-size: 11px !important; }
        .bw-remove-button:hover { background-color: #333 !important; border-color: #fff !important; }

        /* Slot Selector */
        .bw-slot-selector { display: flex !important; gap: 5px !important; margin: 0 10px !important; }
        .bw-slot-btn { background-color: #000 !important; color: #fff !important; border: 1px solid #444 !important; padding: 5px 10px !important; border-radius: 3px !important; cursor: pointer !important; font-size: 11px !important; }
        .bw-slot-btn.active { background-color: #000 !important; border-color: #fff !important; box-shadow: 0 0 5px rgba(255,255,255,0.5) !important; }

        /* "I'm in" / "Leave" Button */
        .bw-im-in-btn { background-color: #000 !important; color: #fff !important; border: 1px solid #fff !important; padding: 6px 15px !important; border-radius: 15px !important; cursor: pointer !important; font-size: 12px !important; font-weight: bold !important; transition: all 0.2s !important; }
        /* Joined State: White Background, Black Text */
        .bw-im-in-btn.joined { background-color: #fff !important; color: #000 !important; border: 1px solid #fff !important; }

        .bw-participants { display: flex !important; flex-wrap: wrap !important; gap: 5px !important; margin-top: 10px !important; }
        .bw-participant-tag { background-color: #000 !important; color: #fff !important; padding: 4px 10px !important; border-radius: 12px !important; font-size: 10px !important; border: 1px solid #444 !important; }
        .bw-loading { text-align: center !important; padding: 20px !important; color: #666 !important; font-style: italic; }
        
        /* STATS DISPLAY & COLORS */
        .bw-stats-input { background: #000 !important; border: 1px solid #333 !important; color: #fff !important; width: 60px !important; font-size: 10px !important; padding: 2px 5px !important; border-radius: 3px !important; margin-top: 5px !important; }
        .bw-stats-display { font-size: 11px !important; font-weight: bold !important; margin-top: 5px !important; display: block !important; }
        
        /* Color Classes for Stats */
        .stat-red { color: #ff4444 !important; }       /* > 20% disadvantage */
        .stat-orange { color: #ffbb33 !important; }    /* 0-20% disadvantage */
        .stat-yellow { color: #ffeb3b !important; }    /* 0-5% advantage */
        .stat-green { color: #00c851 !important; }     /* 5-10% advantage */
        .stat-lightgreen { color: #69f0ae !important; }/* 10-30% advantage */
        .stat-blue { color: #33b5e5 !important; }      /* > 30% advantage (safe) */
        .stat-white { color: #fff !important; }        /* default/unknown */
        `);

        // CREATE ELEMENTS
        const button = document.createElement('button');
        button.id = 'bw-war-button';
        button.textContent = 'BW WAR';
        document.body.appendChild(button);

        const overlay = document.createElement('div');
        overlay.id = 'bw-war-overlay';
        document.body.appendChild(overlay);

        const modal = document.createElement('div');
        modal.id = 'bw-war-modal';
        modal.innerHTML = `
            <div class="bw-modal-header">
                <div class="bw-header-buttons"><button class="bw-settings-button">⚙️</button><button class="bw-close-button">&times;</button></div>
                <img src="https://factionimages.torn.com/08099e78-0a69-4424-8ccd-5219338250ad-40039.png" class="bw-modal-logo" style="max-width: 200px; height: 50px; margin-bottom: 10px;">
                <div class="bw-modal-title">Black & White War Manager</div>
            </div>
            <div class="bw-tabs">
                <button class="bw-tab active" data-tab="info">Info</button>
                <button class="bw-tab" data-tab="team">Team</button>
                <button class="bw-tab" data-tab="targets">Targets</button>
                <button class="bw-tab" data-tab="assists">Assists</button>
            </div>
            <div class="bw-tab-content active" id="info-content"><div class="bw-loading">Loading your information...</div></div>
            <div class="bw-tab-content" id="team-content"><div class="bw-loading">Loading faction members...</div></div>
            <div class="bw-tab-content" id="targets-content">
                <div class="bw-input-section" id="admin-import-section" style="display:none;">
                    <input type="text" id="faction-import-input" class="bw-input-field" placeholder="Faction ID to Import (Admin Only)">
                    <button id="import-faction-btn" class="bw-add-button">Load Faction</button>
                </div>
                <div id="targets-list"></div>
            </div>
            <div class="bw-tab-content" id="assists-content">
                <div class="bw-input-section">
                    <input type="text" id="assist-input" class="bw-input-field" placeholder="Enter User ID or Name" style="flex: 2;">
                    <div class="bw-slot-selector">
                        <button class="bw-slot-btn active" data-slots="1">+1</button><button class="bw-slot-btn" data-slots="2">+2</button><button class="bw-slot-btn" data-slots="3">+3</button><button class="bw-slot-btn" data-slots="4">+4</button>
                    </div>
                    <button id="add-assist-btn" class="bw-add-button">Add</button>
                </div>
                <div id="assists-list"></div>
            </div>
        `;
        document.body.appendChild(modal);

        const settingsModal = document.createElement('div');
        settingsModal.id = 'bw-settings-modal';
        settingsModal.innerHTML = `
            <div class="bw-modal-header">
                <div class="bw-header-buttons"><button class="bw-close-button settings-close">&times;</button></div>
                <img src="https://factionimages.torn.com/08099e78-0a69-4424-8ccd-5219338250ad-40039.png" class="bw-modal-logo" style="max-width: 200px; height: 50px; margin-bottom: 10px;">
                <div class="bw-modal-title">Settings</div>
            </div>
            <div style="padding: 30px; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 60%;">
                <div style="width: 100%; max-width: 400px;">
                    <label style="display: block; margin-bottom: 10px; font-weight: bold; color: #fff; text-align: center;">Torn API Key</label>
                    <div style="display: flex; gap: 10px;">
                        <input type="text" id="api-key-input" class="bw-input-field" placeholder="Enter key" style="text-align: center;">
                        <button id="save-api-key" class="bw-add-button">Save</button>
                    </div>
                </div>
                <div style="margin-top: 30px; padding: 15px; background-color: #111; border: 1px solid #333; border-radius: 5px; font-size: 12px; color: #888; text-align: center; width: 100%; max-width: 400px;">
                    Requires Full Access API key for faction data.<br>
                    <a href="https://www.torn.com/preferences.php#tab=api" target="_blank" style="color: #fff; text-decoration: underline;">Get Key Here</a>
                </div>
            </div>
        `;
        document.body.appendChild(settingsModal);

        // HELPERS
        function getApiKey() { return GM_getValue('torn_api_key', ''); }

        function tornApiCall(endpoint, apiKey = null) {
            return new Promise((resolve, reject) => {
                const key = apiKey || getApiKey();
                if (!key) { reject('No API key set'); return; }
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.torn.com/${endpoint}&key=${key}`,
                    onload: function(response) {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.error) { reject(data.error.error); } else { resolve(data); }
                        } catch (e) { reject('Failed to parse response'); }
                    },
                    onerror: function() { reject('Network error'); }
                });
            });
        }

        function formatBigNumber(num) {
            if (!num) return '0';
            if (num >= 1e9) return parseFloat((num / 1e9).toFixed(2)) + 'B';
            if (num >= 1e6) return parseFloat((num / 1e6).toFixed(2)) + 'M';
            if (num >= 1e3) return parseFloat((num / 1e3).toFixed(2)) + 'K';
            return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
        }

        function getColorForStats(targetStats, userTotalStats) {
            if (!targetStats || !userTotalStats) return "stat-white";
            
            const tStats = parseFloat(targetStats) * 1000; // Convert K to actual
            const uStats = parseFloat(userTotalStats);
            
            // Logic:
            // t > u (20%+) -> Red
            if (tStats > uStats * 1.2) return "stat-red";
            // t > u (0-20%) -> Orange
            if (tStats > uStats) return "stat-orange";
            // u > t (0-5% adv) -> Yellow
            if (tStats >= uStats * 0.95) return "stat-yellow";
            // u > t (5-10% adv) -> Green
            if (tStats >= uStats * 0.90) return "stat-green";
            // u > t (10-30% adv) -> Light Green
            if (tStats >= uStats * 0.70) return "stat-lightgreen";
            // u > t (>50% adv - safe) -> Blue
            return "stat-blue";
        }

        // --- DB FUNCTIONS (REST API) ---
        
        function fetchData() {
            if (isPolling) return;
            isPolling = true;
            
            // Fetch Assists
            GM_xmlhttpRequest({
                method: "GET",
                url: `${DB_URL}/assists.json`,
                onload: function(res) {
                    if(res.status === 200) {
                        const data = JSON.parse(res.responseText);
                        assists = data ? Object.keys(data).map(key => ({ ...data[key], firebaseId: key })) : [];
                        renderAssists();
                    }
                    // Fetch Targets
                    GM_xmlhttpRequest({
                        method: "GET",
                        url: `${DB_URL}/targets.json`,
                        onload: function(res2) {
                            isPolling = false;
                            if(res2.status === 200) {
                                const data2 = JSON.parse(res2.responseText);
                                targetsList = data2 ? Object.keys(data2).map(key => ({ ...data2[key], firebaseId: key })) : [];
                                renderTargets();
                            }
                        },
                        onerror: function() { isPolling = false; }
                    });
                },
                onerror: function() { isPolling = false; }
            });
        }

        function startDBPolling() {
            fetchData();
            const poll = () => { setTimeout(() => { fetchData(); poll(); }, 4000); };
            poll();
        }

        function addItemToDB(node, item) {
            return new Promise((resolve, reject) => {
                if (!currentUser) { alert("User info missing!"); reject(); return; }
                item.addedBy = currentUser.name;
                item.timestamp = Date.now();
                item.customStats = ""; 

                GM_xmlhttpRequest({
                    method: "POST",
                    url: `${DB_URL}/${node}.json`,
                    data: JSON.stringify(item),
                    headers: { "Content-Type": "application/json" },
                    onload: function(res) {
                        if(res.status === 200) { fetchData(); resolve(true); } 
                        else { alert("DB Error"); reject(res.statusText); }
                    }
                });
            });
        }

        function updateItemInDB(node, firebaseId, updates) {
            // Optimistic UI update handled in render
            GM_xmlhttpRequest({
                method: "PATCH",
                url: `${DB_URL}/${node}/${firebaseId}.json`,
                data: JSON.stringify(updates),
                onload: function() { fetchData(); }
            });
        }

        function removeItemFromDB(node, firebaseId) {
            GM_xmlhttpRequest({
                method: "DELETE",
                url: `${DB_URL}/${node}/${firebaseId}.json`,
                onload: function() { fetchData(); }
            });
        }
        
        function clearTargetsDB() {
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: "DELETE",
                    url: `${DB_URL}/targets.json`,
                    onload: function() { resolve(); }
                });
            });
        }

        // --- RENDER FUNCTIONS ---

        async function loadUserInfo() {
            const container = document.querySelector('#info-content');
            try {
                const data = await tornApiCall('user/?selections=profile,battlestats,personalstats');
                currentUser = data;
                
                // Show/Hide Admin Import Input
                const adminSection = document.getElementById('admin-import-section');
                if (adminSection) {
                    adminSection.style.display = (currentUser.player_id === ADMIN_ID) ? 'flex' : 'none';
                }

                const bsTotal = (data.strength || 0) + (data.defense || 0) + (data.speed || 0) + (data.dexterity || 0);
                currentUser.totalStats = bsTotal; // Save for comparison

                container.innerHTML = `
                    <div style="max-width: 600px; margin: 0 auto;">
                        <div style="background-color: #111; border: 1px solid #333; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
                            <a href="https://www.torn.com/profiles.php?XID=${data.player_id}" target="_blank" style="color: #fff; font-size: 20px; font-weight: bold; text-decoration: none;">${data.name} [${data.player_id}]</a>
                            <span style="color: #888; margin-left: 10px;">Level ${data.level}</span>
                            
                            <div style="margin-top: 20px;">
                                <div style="color: #fff; font-weight: bold; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 5px;">Battle Stats</div>
                                <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
                                    <div style="background: #000; padding: 10px; border-radius: 5px; border: 1px solid #222;"><div style="color: #888; font-size: 10px;">Strength</div><div style="color: #fff; font-weight: bold;">${formatBigNumber(data.strength)}</div></div>
                                    <div style="background: #000; padding: 10px; border-radius: 5px; border: 1px solid #222;"><div style="color: #888; font-size: 10px;">Defense</div><div style="color: #fff; font-weight: bold;">${formatBigNumber(data.defense)}</div></div>
                                    <div style="background: #000; padding: 10px; border-radius: 5px; border: 1px solid #222;"><div style="color: #888; font-size: 10px;">Speed</div><div style="color: #fff; font-weight: bold;">${formatBigNumber(data.speed)}</div></div>
                                    <div style="background: #000; padding: 10px; border-radius: 5px; border: 1px solid #222;"><div style="color: #888; font-size: 10px;">Dexterity</div><div style="color: #fff; font-weight: bold;">${formatBigNumber(data.dexterity)}</div></div>
                                </div>
                                <div style="background: #000; padding: 10px; border-radius: 5px; margin-top: 10px; border: 1px solid #fff; text-align: center;">
                                    <div style="color: #ccc; font-size: 12px;">Total Stats</div>
                                    <div style="color: #fff; font-weight: bold; font-size: 18px;">${formatBigNumber(bsTotal)}</div>
                                </div>
                            </div>
                        </div>
                    </div>`;
            } catch (error) {
                container.innerHTML = `<div style="text-align: center; padding: 40px; color: #fff;">Error: ${error}<br>Check Settings.</div>`;
            }
        }

        async function loadFactionMembers() {
            const container = document.querySelector('#team-content');
            try {
                const data = await tornApiCall('faction/?selections=basic');
                if (!data.members) throw new Error('No data');
                
                factionMembers = Object.keys(data.members).map(id => {
                    const member = data.members[id];
                    // Try to guess rank priority (Leader > Co > ... > Recruit) - simplified string sort usually works ok-ish or mapping
                    return { id: id, name: member.name, position: member.position, status: member.status.state };
                });

                // Sort by Position (Simplified Alphabetical Sort for now, helps group ranks)
                factionMembers.sort((a, b) => a.position.localeCompare(b.position));

                container.innerHTML = `<div style="color: #fff; font-weight: bold; margin-bottom: 15px; font-size: 18px;">Faction Members (${factionMembers.length})</div>
                    ${factionMembers.map(member => `<div class="bw-member-item">
                        <a href="https://www.torn.com/profiles.php?XID=${member.id}" target="_blank" class="bw-member-name">${member.name} [${member.id}]</a>
                        <span class="bw-rank-badge">${member.position}</span>
                    </div>`).join('')}`;
            } catch (error) {
                container.innerHTML = `<div style="text-align: center; padding: 40px; color: #888;">Error loading faction. Check API Key.</div>`;
            }
        }

        function renderList(nodeType, listData, containerId) {
            const list = document.getElementById(containerId);
            if (listData.length === 0) { list.innerHTML = '<div style="text-align: center; color: #666; padding: 20px;">Empty list</div>'; return; }
            
            const isAdmin = currentUser && currentUser.player_id === ADMIN_ID;
            const userTotal = currentUser ? currentUser.totalStats : 0;

            list.innerHTML = listData.map((item, index) => {
                const isJoined = currentUser && item.participants && Object.values(item.participants).includes(currentUser.name);
                const btnClass = isJoined ? "bw-im-in-btn joined" : "bw-im-in-btn";
                const btnText = isJoined ? "Leave" : "I'm in";
                const participantsList = item.participants ? Object.values(item.participants) : [];
                
                // Stats Logic
                let statsHtml = '';
                if (isAdmin) {
                    statsHtml = `<input type="text" class="bw-stats-input" data-node="${nodeType}" data-id="${item.firebaseId}" value="${item.customStats || ''}" placeholder="Stats(k)">`;
                } else if (item.customStats) {
                    const colorClass = getColorForStats(item.customStats, userTotal);
                    statsHtml = `<span class="bw-stats-display ${colorClass}">${item.customStats}k Stats</span>`;
                }

                // Slots display (only for assists)
                const slotHtml = nodeType === 'assists' ? `<span style="color: #666; margin-left: 10px; font-size: 12px;">(${item.slots} slots)</span>` : '';
                
                // Toggle Button (only for assists, Targets just has Attack)
                let actionButtons = '';
                if (nodeType === 'assists') {
                    actionButtons = `<button class="${btnClass}" data-index="${index}">${btnText}</button>`;
                }

                return `
                <div class="${nodeType === 'assists' ? 'bw-assist-item' : 'bw-target-item'}" style="flex-direction: column; align-items: flex-start;">
                    <div style="width: 100%; display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px;">
                        <div>
                            <a href="https://www.torn.com/profiles.php?XID=${item.id}" target="_blank" class="${nodeType === 'assists' ? 'bw-target-name' : 'bw-member-name'}">
                                ${item.name} [${item.id}] <span style="color:#444; font-size:10px;">Lvl ${item.level}</span>
                            </a>
                            ${slotHtml}
                            ${statsHtml}
                        </div>
                        <div>
                            ${actionButtons}
                            <button class="bw-add-button" data-id="${item.id}" style="margin: 0 5px; font-size: 11px; padding: 5px 10px;">Attack</button>
                            <button class="bw-remove-button" data-node="${nodeType}" data-id="${item.firebaseId}">Remove</button>
                        </div>
                    </div>
                    ${nodeType === 'assists' && participantsList.length > 0 ? `<div class="bw-participants">${participantsList.map(p => `<span class="bw-participant-tag">${p}</span>`).join('')}</div>` : ''}
                </div>`;
            }).join('');

            // Listeners
            list.querySelectorAll('.bw-add-button').forEach(btn => btn.addEventListener('click', (e) => window.open(`https://www.torn.com/loader.php?sid=attack&user2ID=${e.target.dataset.id}`, '_blank')));
            list.querySelectorAll('.bw-remove-button').forEach(btn => btn.addEventListener('click', (e) => { 
                if(confirm('Remove this item?')) removeItemFromDB(e.target.dataset.node, e.target.dataset.id); 
            }));

            // Stats Input Listener (Admin)
            if (isAdmin) {
                list.querySelectorAll('.bw-stats-input').forEach(input => {
                    input.addEventListener('change', (e) => {
                        updateItemInDB(e.target.dataset.node, e.target.dataset.id, { customStats: e.target.value });
                    });
                });
            }

            // Toggle Join Listener (Assists Only)
            if (nodeType === 'assists') {
                list.querySelectorAll('.bw-im-in-btn').forEach(btn => btn.addEventListener('click', async (e) => {
                    const index = parseInt(e.target.dataset.index);
                    const item = listData[index];
                    if (!currentUser) return;
                    let parts = item.participants ? Object.values(item.participants) : [];
                    if (parts.includes(currentUser.name)) { parts = parts.filter(p => p !== currentUser.name); }
                    else { 
                        if (parts.length >= item.slots) { alert('Full!'); return; }
                        parts.push(currentUser.name); 
                    }
                    updateItemInDB('assists', item.firebaseId, { participants: parts });
                }));
            }
        }

        function renderAssists() { renderList('assists', assists, 'assists-list'); }
        function renderTargets() { renderList('targets', targetsList, 'targets-list'); }

        // BUTTON ACTIONS
        document.getElementById('add-assist-btn').addEventListener('click', async () => {
            const val = document.getElementById('assist-input').value.trim();
            if (!val) return;
            try {
                let data = await tornApiCall(`user/${val}?selections=profile`);
                if (!data || !data.player_id) { alert('User not found'); return; }
                let slots = parseInt(document.querySelector('.bw-slot-btn.active').dataset.slots);
                await addItemToDB('assists', { id: data.player_id, name: data.name, level: data.level, slots: slots, participants: [] });
                document.getElementById('assist-input').value = '';
            } catch (e) { alert('Error: ' + e); }
        });

        document.getElementById('add-mytarget-btn').addEventListener('click', async () => {
            const val = document.getElementById('mytarget-input').value.trim();
            if (!val) return;
            try {
                let data = await tornApiCall(`user/${val}?selections=profile`);
                if (!data || !data.player_id) { alert('User not found'); return; }
                await addItemToDB('targets', { id: data.player_id, name: data.name, level: data.level });
                document.getElementById('mytarget-input').value = '';
            } catch (e) { alert('Error: ' + e); }
        });

        // ADMIN FACTION IMPORT
        const importBtn = document.getElementById('import-faction-btn');
        if (importBtn) {
            importBtn.addEventListener('click', async () => {
                const fid = document.getElementById('faction-import-input').value.trim();
                if (!fid) return;
                if (!confirm(`This will REPLACE the current target list with all members of Faction ${fid}. Continue?`)) return;
                
                try {
                    const data = await tornApiCall(`faction/${fid}?selections=basic`);
                    if (!data.members) throw "No members found";
                    
                    await clearTargetsDB(); // Wipe current list
                    
                    // Add all members
                    const members = Object.values(data.members);
                    alert(`Importing ${members.length} members... please wait.`);
                    
                    for (let m of members) {
                        // Small delay to not hammer Firebase too hard sequentially, though it handles it
                        await addItemToDB('targets', { id: m.player_id, name: m.name, level: m.level });
                    }
                    alert("Import Complete!");
                } catch (e) { alert("Import Failed: " + e); }
            });
        }

        // SLOT SELECTOR LOGIC
        document.querySelectorAll('.bw-slot-btn').forEach(btn => btn.addEventListener('click', (e) => { 
            document.querySelectorAll('.bw-slot-btn').forEach(b => b.classList.remove('active')); 
            e.target.classList.add('active'); 
        }));

        // MODAL LOGIC
        function openModal() {
             document.getElementById('bw-war-modal').classList.add('active'); 
             document.getElementById('bw-war-overlay').classList.add('active');
             document.getElementById('bw-settings-modal').classList.remove('active');
             loadUserInfo(); loadFactionMembers(); startDBPolling();
        }
        function closeModals() { 
            document.getElementById('bw-war-modal').classList.remove('active'); 
            document.getElementById('bw-settings-modal').classList.remove('active'); 
            document.getElementById('bw-war-overlay').classList.remove('active'); 
        }

        document.getElementById('bw-war-button').addEventListener('click', openModal);
        document.querySelector('.bw-settings-button').addEventListener('click', () => {
            document.getElementById('bw-war-modal').classList.remove('active');
            document.getElementById('bw-settings-modal').classList.add('active');
            document.getElementById('api-key-input').value = getApiKey();
        });
        document.getElementById('bw-war-overlay').addEventListener('click', closeModals);
        document.querySelectorAll('.bw-close-button').forEach(b => b.addEventListener('click', closeModals));
        document.getElementById('save-api-key').addEventListener('click', () => {
            const key = document.getElementById('api-key-input').value.trim();
            if(key) { GM_setValue('torn_api_key', key); alert('Saved'); closeModals(); }
        });

        // TABS LOGIC
        const tabs = document.querySelectorAll('.bw-tab');
        const contents = document.querySelectorAll('.bw-tab-content');
        tabs.forEach(tab => tab.addEventListener('click', () => {
            tabs.forEach(t => t.classList.remove('active')); 
            contents.forEach(c => c.classList.remove('active'));
            tab.classList.add('active'); 
            document.getElementById(`${tab.dataset.tab}-content`).classList.add('active');
        }));
    }
    initScript();
})();