HW: Clan Shop

HackerWars Clan Shop

// ==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);

})();