HW: Clan Shop

HackerWars Clan Shop

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         HW: Clan Shop
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  HackerWars Clan Shop
// @match        https://hackerwars.io/clan
// @match        https://hackerwars.io/clan?id=*
// @author       Nacom
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const PROCESSED_ATTR = 'data-hw-shop-parsed';
    const cart = [];

    const style = document.createElement('style');
    style.textContent = `
    @keyframes rainbow {
        0%{color:red} 16%{color:orange} 33%{color:yellow} 50%{color:green} 66%{color:blue} 83%{color:indigo} 100%{color:violet}
    }
    .rainbow-text{animation:rainbow 2s linear infinite}
    `;
    document.head.appendChild(style);

    function findClanDescriptionElement() {
        const titleEls = Array.from(document.querySelectorAll('.widget-box .widget-title h5'));
        const descTitle = titleEls.find(h => h.textContent.trim() === 'Clan description');
        if(!descTitle) return null;
        const widgetBox = descTitle.closest('.widget-box');
        if(!widgetBox) return null;
        return widgetBox.querySelector('.widget-content.padding') || null;
    }

    function parseDescription(text) {
        const result = { owner:null, products:[] };
        const ownerMatch = text.match(/shopowner\s*:\s*(\S+)/i);
        if(ownerMatch) result.owner = ownerMatch[1];

        const softwareMatch = text.match(/software\s*:\s*([\s\S]*)/i);
        if(!softwareMatch) return result;

        const productRegex = /- ([^\(\n]+)\(([^)]+)\)/g;
        let match;
        while((match = productRegex.exec(softwareMatch[1]))!==null){
            const prodName = match[1].trim();
            const versions = match[2].trim().split(',').map(v=>{
                const [name, price] = v.split(':');
                return { name:name.trim(), price:parseFloat(price) };
            });
            result.products.push({ name:prodName, versions });
        }
        return result;
    }

    function formatPrice(num){ return num.toFixed(3).replace(/\B(?=(\d{3})+(?!\d))/g,'.'); }

    function processDescription(text){
        let html = text.replace(/NCSS!Break/gi,'<br>');
        html = html.replace(/\?rainbow!(.*?)!rainbow\?/gs,'<span class="rainbow-text">$1</span>');
        html = html.replace(/\?(red|blue|green|yellow|purple|orange)!(.*?)!\1\?/gs,(m,color,content)=>`<span style="color:${color}">${content}</span>`);
        return html;
    }

    function createCartWidget(shopWrapper){
        const cartWidget = document.createElement('div');
        cartWidget.style.position='absolute';
        cartWidget.style.top='10px';
        cartWidget.style.right='10px';
        cartWidget.style.width='32px';
        cartWidget.style.height='32px';
        cartWidget.style.cursor='pointer';

        const icon = document.createElement('div');
        icon.textContent='🛒';
        icon.style.fontSize='28px';
        icon.style.position='relative';
        cartWidget.appendChild(icon);

        const countBubble = document.createElement('span');
        countBubble.textContent='0';
        countBubble.style.position='absolute';
        countBubble.style.top='-6px';
        countBubble.style.right='-6px';
        countBubble.style.background='red';
        countBubble.style.color='white';
        countBubble.style.borderRadius='50%';
        countBubble.style.width='18px';
        countBubble.style.height='18px';
        countBubble.style.fontSize='12px';
        countBubble.style.textAlign='center';
        countBubble.style.lineHeight='18px';
        icon.appendChild(countBubble);

        const tooltip=document.createElement('div');
        tooltip.style.position='absolute';
        tooltip.style.top='36px';
        tooltip.style.right='0';
        tooltip.style.width='220px';
        tooltip.style.background='#fff';
        tooltip.style.border='1px solid #ccc';
        tooltip.style.borderRadius='6px';
        tooltip.style.padding='8px';
        tooltip.style.boxShadow='0 1px 4px rgba(0,0,0,0.2)';
        tooltip.style.display='none';
        tooltip.style.zIndex='1000';
        shopWrapper.appendChild(tooltip);

        cartWidget.addEventListener('mouseenter',()=>{tooltip.style.display='block'; renderTooltip();});
        cartWidget.addEventListener('mouseleave',()=>{tooltip.style.display='none';});

        function renderTooltip(){
            tooltip.innerHTML='';
            if(cart.length===0){tooltip.textContent='Cart is empty'; return;}
            let sum=0;
            cart.forEach(item=>{
                const line=document.createElement('div');
                line.style.display='flex';
                line.style.justifyContent='space-between';
                line.style.marginBottom='4px';
                line.textContent=`${item.name} (${item.version}) $${formatPrice(item.price)}`;
                tooltip.appendChild(line);
                sum+=item.price;
            });
            const total=document.createElement('div');
            total.style.fontWeight='bold';
            total.style.textAlign='right';
            total.textContent=`Total: $${formatPrice(sum)}`;
            tooltip.appendChild(total);
        }

        function updateCount(){countBubble.textContent=cart.length;}

        shopWrapper.appendChild(cartWidget);
        return { updateCount, renderTooltip };
    }

    function renderShop(container,data){
        container.innerHTML='';
        container.style.position='relative';

        const shopWrapper=document.createElement('div');
        shopWrapper.className='widget-box';
        shopWrapper.style.marginTop='10px';

        const header=document.createElement('div');
        header.className='widget-title';
        header.innerHTML=`<span class="icon"><span class="he16-clan_desc"></span></span><h5>Shop</h5>`;
        shopWrapper.appendChild(header);

        const contentDiv=document.createElement('div');
        contentDiv.className='widget-content padding';
        contentDiv.style.position='relative';
        shopWrapper.appendChild(contentDiv);

        const cartUI=createCartWidget(contentDiv);

        data.products.forEach(product=>{
            const line=document.createElement('div');
            line.style.display='flex';
            line.style.alignItems='center';
            line.style.marginTop='6px';
            line.style.border='1px solid #ddd';
            line.style.borderRadius='6px';
            line.style.padding='8px';

            const nameDiv=document.createElement('div');
            nameDiv.textContent=product.name;
            nameDiv.style.flex='1';
            line.appendChild(nameDiv);

            const versionSelect=document.createElement('select');
            versionSelect.style.marginRight='10px';

            let maxVer=Math.max(...product.versions.map(v=>parseFloat(v.name)));
            product.versions.forEach(v=>{
                const opt=document.createElement('option');
                opt.value=v.price;
                opt.textContent=v.name;
                if(parseFloat(v.name)===maxVer) opt.classList.add('rainbow-text');
                versionSelect.appendChild(opt);
            });
            line.appendChild(versionSelect);

            const priceSpan=document.createElement('span');
            priceSpan.textContent='$'+formatPrice(product.versions[0]?.price||0);
            priceSpan.style.width='60px';
            line.appendChild(priceSpan);

            versionSelect.addEventListener('change',()=>{
                priceSpan.textContent='$'+formatPrice(parseFloat(versionSelect.value));
            });

            const addBtn=document.createElement('button');
            addBtn.textContent='Add to Cart';
            addBtn.style.marginLeft='10px';
            addBtn.style.background='#5DADE2';
            addBtn.style.color='white';
            addBtn.style.border='none';
            addBtn.style.borderRadius='4px';
            addBtn.style.padding='6px 10px';
            addBtn.style.cursor='pointer';
            addBtn.addEventListener('click',()=>{
                const selectedVer=versionSelect.options[versionSelect.selectedIndex].textContent;
                const selectedPrice=parseFloat(versionSelect.value);
                if(cart.some(item=>item.name===product.name&&item.version===selectedVer)) return alert('This version is already in the cart!');
                cart.push({name:product.name,version:selectedVer,price:selectedPrice});
                cartUI.updateCount();
                cartUI.renderTooltip();
            });
            line.appendChild(addBtn);

            contentDiv.appendChild(line);
        });

        const orderBtn=document.createElement('button');
        orderBtn.textContent='Order Cart';
        orderBtn.style.marginTop='12px';
        orderBtn.style.background='#27ae60';
        orderBtn.style.color='white';
        orderBtn.style.border='none';
        orderBtn.style.borderRadius='4px';
        orderBtn.style.padding='6px 10px';
        orderBtn.style.cursor='pointer';
        orderBtn.addEventListener('click',async()=>{
            if(!data.owner) return alert('No shop owner found');
            if(cart.length===0) return alert('Cart empty!');
            const userEl=document.querySelector('#user-nav .text');
            const username=userEl?userEl.textContent.trim():'Unknown';
            let body=`New Order from ${username}\nOrdered:\n`;
            cart.forEach(item=>{body+=`- ${item.name} (${item.version})\n`;});
            try{
                await fetch('/mail.php?action=new',{
                    method:'POST',
                    headers:{'Content-Type':'application/x-www-form-urlencoded'},
                    body:new URLSearchParams({
                        action:'new',
                        act:'new',
                        to:data.owner,
                        subject:'New Shop Order',
                        text:body
                    })
                });
                alert('Order sent!');
                cart.length=0;
                cartUI.updateCount();
                cartUI.renderTooltip();
            }catch(e){alert('Failed to send order'); console.error(e);}
        });
        contentDiv.appendChild(orderBtn);

        container.appendChild(shopWrapper);
    }

    const contentEl=findClanDescriptionElement();
    if(!contentEl || contentEl.getAttribute(PROCESSED_ATTR)) return;
    contentEl.setAttribute(PROCESSED_ATTR,'1');

    const parsed=parseDescription(contentEl.textContent);
    const descText=contentEl.textContent.replace(/(shopowner\s*:.*|software\s*:.*|- [^\(\n]+\([^)]+\))/gi,'');
    contentEl.innerHTML=processDescription(descText);

    const shopContainer=document.createElement('div');
    contentEl.parentElement.appendChild(shopContainer);
    renderShop(shopContainer,parsed);

})();