DeepCo NekHack

Mini game

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         DeepCo NekHack
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  Mini game
// @match        https://deepco.app/*
// @license      MIT
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=deepco.app
// ==/UserScript==

(function () {
    'use strict';

    const EXTENSION_ID = 'nek-hack';
    const EXTENSION_LABEL = 'NekHack';
    const EXTENSION_VERSION = (typeof GM !== 'undefined' && GM?.info?.script?.version) || '0.2.1';
    const BUTTON_COLOR = '#ff0000';

    const INJECT_TARGET = 'footer.footer>nav';
    const INJECT_BEFORE_ID = 'tip-area';
    const GRAPH_WRAPPER_ID = 'nek-graph-wrapper';

    const SCREEN_WRAPPER_ID = 'nek-arcade-wrapper';
    const SCREEN_CANVAS_ID = 'nek-arcade-screen';
    const PANEL_ID = 'nek-arcade-panel';
    const TRACK_ID = 'nek-arcade-risk-track';

    const POLL_INTERVAL = 2000;
    const BET_OPTIONS = [10, 100, 1000];
    const RISK_MIN = 0;
    const RISK_MAX = 10;

    const minRiskChance = 0.33;
    const maxRiskChance = 0.10;
    const baseRTP = 0.7;

    let creditsLabel;

    let prefsJson = {};
    try {
        prefsJson = JSON.parse(localStorage.getItem('nek-arcade-prefs')) || {};
    } catch (e) {
        console.error(e);
    }
    const prefs = {
        show: true,
        ...prefsJson,
    };
    function savePrefs() {
        localStorage.setItem('nek-arcade-prefs', JSON.stringify(prefs));
    }

    let stateJson = {};
    try {
        const rawData = localStorage.getItem('nek-arcade-state');
        if (rawData) {
            const decodedData = atob(rawData);
            stateJson = JSON.parse(decodedData);
        }
    } catch (e) {
        console.error(e);
    }

    const state = {
        credits: 0,
        nekots: 0,
        lastBrokenBlocksTotal: null,
        bet: 10,
        risk: 5,
        lastResult: '',
        ...stateJson,
    };

    function saveState() {
        try {
            const jsonString = JSON.stringify(state);
            const encodedData = btoa(jsonString);
            localStorage.setItem('nek-arcade-state', encodedData);
        } catch (e) {
            console.error(e);
        }
    }
    class TabLeader {
        constructor(channelName, { onGainLeadership, onLoseLeadership } = {}) {
            this._channel = new BroadcastChannel(channelName);
            this._isLeader = false;
            this._electTimeout = null;
            this._onGain = onGainLeadership ?? (() => {});
            this._onLose = onLoseLeadership ?? (() => {});

            this._channel.onmessage = ({ data }) => {
                if (data === 'takeover') {
                    clearTimeout(this._electTimeout);
                    this._electTimeout = null;
                    if (this._isLeader) {
                        this._isLeader = false;
                        this._onLose();
                    }
                } else if (data === 'leader-gone') {
                    this._electTimeout = setTimeout(
                        () => this._claim(),
                        Math.random() * 150 + 50
                    );
                }
            };

            window.addEventListener('beforeunload', () => this.destroy());
        }

        claim() {
            this._claim();
        }

        get isLeader() {
            return this._isLeader;
        }

        destroy() {
            clearTimeout(this._electTimeout);
            if (this._isLeader) {
                this._isLeader = false;
                this._channel.postMessage('leader-gone');
            }
            this._channel.close();
        }

        _claim() {
            this._electTimeout = null;
            this._isLeader = true;
            this._channel.postMessage('takeover');
            this._onGain();
        }
    }

    let leader, isLeader = false;

    function scrapeBrokenBlocks() {
        const el = document.getElementById('tiles-defeated-badge')?.children[0];
        if (!el) return null;
        const parsed = parseInt(el.textContent.split('/')[0].replace(/[,.\s]/g, ''));
        return isNaN(parsed) ? null : parsed;
    }

    function collectCredits() {
        const total = scrapeBrokenBlocks();
        if (total === null) return;

        if (state.lastBrokenBlocksTotal === null) {
            state.lastBrokenBlocksTotal = total;
            saveState();
            return;
        }

        const delta = total - state.lastBrokenBlocksTotal;
        if (delta > 0) {
            state.credits += delta;
            state.lastBrokenBlocksTotal = total;
            saveState();
            onCreditsChanged();
        } else if (delta < 0) {
            state.lastBrokenBlocksTotal = total;
            saveState();
        }
    }

    function startCreditCollector() {
        setInterval(() => {
            if (!isLeader) return;
            collectCredits();
        }, POLL_INTERVAL);
    }

    const screenWrapper = document.createElement('div');
    screenWrapper.id = SCREEN_WRAPPER_ID;
    screenWrapper.className = 'card bg-base-200 border border-base-300 p-4 mb-4 mx-2';
    screenWrapper.style.cssText = "background: #05070a; overflow: scroll";

    let cursorOn = true;
    let spinText = null;
    let isSpinning = false;
    const SPIN_CHARS = '0123456789#$%&@+-';
    const SPIN_LENGTH = 10;
    const SPIN_TICKS_MS = [35, 35, 40, 45, 55, 65, 80, 100];

    function randomSpinChunk() {
        let out = '';
        for (let i = 0; i < SPIN_LENGTH; i++) {
            out += SPIN_CHARS[Math.floor(Math.random() * SPIN_CHARS.length)];
        }
        return out;
    }

    function renderScreenUI() {
        screenWrapper.innerHTML = `
            <div id="${SCREEN_CANVAS_ID}" style="
                background: #05070a;
                color: #ff0000;
                font-family: 'Courier New', monospace;
                font-size: 13px;
                line-height: 1.3;
                padding: 10px;
                border-radius: 4px 4px 0 0;
                overflow: hidden;
                white-space: pre;
                width: 400px;
                margin:auto;
                user-select: none;
            "></div>
            <div id="${PANEL_ID}" style="
                background: #05070a;
                border-radius: 0 0 4px 4px;
                font-family: 'Courier New', monospace;
                font-size: 13px;
                color: #ff0000;
                user-select: none;
                width: 400px;
                min-width: max-content;
                margin:auto;
            "></div>
        `;
        drawFrame();
        renderControls();
    }

    function drawFrame() {
        const screen = document.getElementById(SCREEN_CANVAS_ID);
        if (!screen) return;

        const width = 48;
        const line = '+' + '-'.repeat(width - 2) + '+';
        const empty = '|' + ' '.repeat(width - 2) + '|';

        const centered = (text, noborders) => {
            const tempEl = document.createElement('div');
            tempEl.innerHTML = text;
            const ctext = tempEl.textContent || tempEl.innerText || "";
            const pad = Math.max(0, width - 2 - ctext.length);
            const left = Math.floor(pad / 2);
            const right = pad - left;
            return (noborders ? ' ':'|') + ' '.repeat(left) + text + ' '.repeat(right) + (noborders ? ' ':'|');
        };

        const resultLine = state.lastResult ? state.lastResult : '';
        const resultHtml = isSpinning
            ? `<span style="color:#ff6600; opacity:0.85;">${spinText}</span>`
            : `<span style="color:#ff6600;">${resultLine}${cursorOn ? '_' : ' '}</span>`;

        const rows = [
            centered('H A C K   T E R M I N A L', true),
            '',
            line,
            empty,
            centered(`CREDS: <span style="color:#F22;">${state.credits}</span>   GOLDS: <span style="color:#F22;">${state.nekots.toFixed(2)}</span>`),
            empty,
            centered(resultHtml),
            empty,
            line,
        ];

        screen.innerHTML = rows.join('\n');
    }

    function onCreditsChanged() {
        drawFrame();
        creditsLabel.textContent = `Credits: ${state.credits}`
    }

    function trackString(value) {
        let out = '[';
        for (let i = RISK_MIN; i <= RISK_MAX; i++) {
            out += i === value ? `-#-` : '---';
        }
        out += ']';
        return out;
    }

    function setRisk(value) {
        const clamped = Math.min(RISK_MAX, Math.max(RISK_MIN, Math.round(value)));
        if (clamped === state.risk) return;
        state.risk = clamped;
        saveState();
        drawFrame();
        renderControls();
    }

    function selectBet(amount) {
        state.bet = amount;
        saveState();
        drawFrame();
        renderControls();
    }

    function winChance(risk) {
        const x = risk / RISK_MAX;
        const force = 10;
        const logRatio = Math.log1p(force * x) / Math.log1p(force);

        return minRiskChance - ((minRiskChance - maxRiskChance) * logRatio);
    }

    function calculateRTP(risk, bet) {
        const riskBonus = 0.15 * (risk / RISK_MAX);

        const minBet = Math.log10(BET_OPTIONS[0]);
        const maxBet = Math.log10(BET_OPTIONS[BET_OPTIONS.length - 1]);
        const betBonus = 0.1 * ((Math.log10(bet) - minBet) / (maxBet - minBet));

        return baseRTP + riskBonus + betBonus;
    }

    function calculateGain(risk, bet) {
        const chance = winChance(risk);
        const rtp = calculateRTP(risk, bet);
        const multiplier = (1 / chance) * (rtp+(1-rtp)/7.77);
        const rawGain = bet * multiplier;
        const pow = Math.pow(10,Math.floor(Math.log10(rawGain)));
        const round = 10
        return Math.floor(Math.ceil(round * bet * multiplier / pow)*pow/round);
    }

    function debugSimulate() {
        console.group(`[NekHack Debug] Simulation du nouvel équilibrage`);


        BET_OPTIONS.forEach((bet) => {
            const tableData = [];
            for (let risk = RISK_MIN; risk <= RISK_MAX; risk++) {
                const chance = winChance(risk);
                const gain = calculateGain(risk, bet);
                const netGain = gain - bet;
                const goldsGain = netGain / 100;

                const expectedValue = (chance * gain) - bet;
                const rtp = (chance * gain / bet) * 100;

                tableData.push({
                    'Bet': bet,
                    'Risk': risk,
                    'Win Chance': `${(chance * 100).toFixed(1)}%`,
                    'Inverted Win Chance': `${(1/chance).toFixed(1)}`,
                    'Gain Brut': gain,
                    'Gain Net': netGain,
                    'Golds Net': goldsGain.toFixed(2),
                    'Golds conversion': (100 * chance * goldsGain / bet).toFixed(3),
                    'EV (Creds)': expectedValue.toFixed(2),
                    'RTP (%)': `${rtp.toFixed(1)}%`
                });
            }
            console.table(tableData);
        });

        console.groupEnd();
    }

    function doConvert() {
        if (state.credits < state.bet) {
            state.lastResult = 'NOT ENOUGH CREDITS';
            saveState();
            drawFrame();
            return;
        }
        state.credits -= state.bet;

        const chance = winChance(state.risk);
        const gain = calculateGain(state.risk, state.bet);

        const roll = Math.random();

        if (roll < chance) {
            const ngain = (gain - state.bet) / 100;
            state.credits += gain;
            state.nekots += ngain;
            state.lastResult = `+${gain} CREDS  \\o/  +${ngain.toFixed(2)} GOLDS`;
        } else {
            state.lastResult = `LOST :(`;
        }
        saveState();
        onCreditsChanged();
    }

    function playConvertAnimation(onDone) {
        isSpinning = true;
        let tickIndex = 0;

        const tick = () => {
            spinText = randomSpinChunk();
            drawFrame();

            if (tickIndex >= SPIN_TICKS_MS.length) {
                isSpinning = false;
                spinText = null;
                onDone();
                return;
            }

            setTimeout(tick, SPIN_TICKS_MS[tickIndex]);
            tickIndex++;
        };

        tick();
    }

    function renderControls() {
        const panel = document.getElementById(PANEL_ID);
        if (!panel) return;

        panel.innerHTML = '';

        const betRow = document.createElement('div');
        betRow.style.cssText = 'display: flex; gap: 6px; margin-bottom: 8px;justify-content: center';

        const betLabel = document.createElement('span');
        betLabel.textContent = 'BET';
        betLabel.style.cssText = `
                font-family: 'Courier New', monospace;
                font-size: 13px;
                padding: 4px 0;
        `;
        betRow.appendChild(betLabel);

        BET_OPTIONS.forEach((amount) => {
            const btn = document.createElement('button');
            const active = state.bet === amount;
            btn.textContent = active ? `[ ${amount} ]` : `\u00A0\u00A0${amount}\u00A0\u00A0`;
            btn.style.cssText = `
                font-family: 'Courier New', monospace;
                font-size: 13px;
                padding: 4px 0;
                border-radius: 4px;
                cursor: pointer;
                color: ${active ? '#f00' : '#800'};
            `;
            btn.onclick = () => selectBet(amount);
            betRow.appendChild(btn);
        });
        panel.appendChild(betRow);

        const trackRow = document.createElement('div');
        trackRow.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 8px; justify-content: center';

        const trackLabel = document.createElement('span');
        trackLabel.textContent = 'RISK';
        trackRow.appendChild(trackLabel);

        const track = document.createElement('div');
        track.id = TRACK_ID;
        track.textContent = trackString(state.risk);
        track.style.cssText = 'flex: 1; letter-spacing: 1px; cursor: pointer; white-space: pre; flex-grow:0';
        const valueFromEvent = (e, rect) => {
            const charWidth = rect.width / trackString(state.risk).length;
            const col = Math.floor((e.clientX - rect.left) / (charWidth*3));
            return RISK_MIN + col;
        };

        track.addEventListener('mousedown', (e) => {
            const rect = track.getBoundingClientRect();
            setRisk(valueFromEvent(e,rect));
            const onMove = (ev) => setRisk(valueFromEvent(ev, rect));
            const onUp = () => {
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            };
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        });

        trackRow.appendChild(track);

        panel.appendChild(trackRow);

        const convertBtn = document.createElement('button');
        convertBtn.textContent = '[ CONVERT ]';
        convertBtn.style.cssText = `
            width: 100%;
            font-family: 'Courier New', monospace;
            font-size: 13px;
            padding: 5px 0;
            cursor: pointer;
            background: transparent;
            color: #ff0000;
        `;
        if (isSpinning) {
            convertBtn.disabled = true;
            convertBtn.style.opacity = '0.5';
            convertBtn.style.cursor = 'default';
        }

        convertBtn.onclick = () => {
            if (isSpinning) return;

            if (state.credits < state.bet) {
                doConvert();
                renderControls();
                return;
            }

            isSpinning = true;
            renderControls();
            playConvertAnimation(() => {
                doConvert();
                renderControls();
            });
        };
        panel.appendChild(convertBtn);
    }

    function startBlinkLoop() {
        setInterval(() => {
            if (!prefs.show) return;
            cursorOn = !cursorOn;
            drawFrame();
        }, 600);
    }

    renderScreenUI();
    if (!prefs.show) {
        screenWrapper.classList.add('hidden');
    }

    function startScreenObserver() {
        function tryInject() {
            const target = document.getElementById(INJECT_BEFORE_ID);
            if (!target) return;
            const parent = target.parentNode;

            if (screenWrapper.parentElement === parent) return;

            const graphWrapper = document.getElementById(GRAPH_WRAPPER_ID);
            if (graphWrapper && graphWrapper.parentElement === parent) {
                parent.insertBefore(screenWrapper, graphWrapper.nextSibling);
            } else {
                parent.insertBefore(screenWrapper, target);
            }

            drawFrame();
            renderControls();
        }
        tryInject();
        new MutationObserver(tryInject).observe(document.documentElement, { childList: true, subtree: true });
    }

    function createMenuContent() {
        const container = document.createElement('div');
        container.style.cssText = 'padding: 5px 0;';

        creditsLabel = document.createElement('div');
        creditsLabel.textContent = `Credits: ${state.credits}`;
        container.appendChild(creditsLabel);

        const toggleWrapper = document.createElement('label');
        toggleWrapper.classList.add('flex', 'items-center', 'gap-2', 'my-3');
        toggleWrapper.dataset.tip = 'Afficher / masquer l\'écran';

        const toggleLabel = document.createElement('span');
        toggleLabel.textContent = 'Show Screen';

        const toggleSwitch = document.createElement('input');
        toggleSwitch.type = 'checkbox';
        toggleSwitch.classList.add('toggle', 'toggle-primary', 'checkbox-xs');
        toggleSwitch.checked = prefs.show;

        toggleSwitch.onchange = () => {
            prefs.show = toggleSwitch.checked;
            savePrefs();
            screenWrapper.classList.toggle('hidden', !prefs.show);
        };

        toggleWrapper.appendChild(toggleSwitch);
        toggleWrapper.appendChild(toggleLabel);
        container.appendChild(toggleWrapper);

        const versionNote = document.createElement('div');
        versionNote.textContent = `${EXTENSION_LABEL} v${EXTENSION_VERSION}`;
        versionNote.style.cssText = 'font-size: 10px; color: #777; text-align: right; padding: 4px 10px 2px; pointer-events: none;';
        container.appendChild(versionNote);

        return container;
    }

    function createStandaloneWidget() {
        const wrapper = document.createElement('div');
        wrapper.style.cssText = 'position: relative; display: inline-block;';

        const btn = document.createElement('button');
        btn.textContent = EXTENSION_LABEL;
        btn.style.color = BUTTON_COLOR;
        btn.classList.add('btn', 'btn-ghost', 'btn-sm', 'text-primary');

        const popup = document.createElement('div');
        popup.style.cssText = 'display: none; position: absolute; bottom: calc(100% + 8px); left: 0; min-width: 200px; z-index: 1000; text-align: left;';
        popup.classList.add('card', 'bg-base-100', 'border', 'border-base-300', 'p-2', 'text-xs', 'shadow-xl');
        popup.appendChild(createMenuContent());

        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
        });
        document.addEventListener('click', (e) => {
            if (!wrapper.contains(e.target)) popup.style.display = 'none';
        });

        wrapper.append(btn, popup);
        return wrapper;
    }

    function injectStandaloneButton() {
        const widget = createStandaloneWidget();
        function tryInject() {
            const target = document.querySelector(INJECT_TARGET);
            if (target && widget.parentElement !== target) target.insertBefore(widget, target.firstChild);
        }
        tryInject();
        new MutationObserver(tryInject).observe(document.documentElement, { childList: true, subtree: true });
    }

    function registerWithNekStyle() {
        window.NekStyle.registerExtension({
            id: EXTENSION_ID,
            label: EXTENSION_LABEL,
            color: BUTTON_COLOR,
            createContent: createMenuContent,
        });
    }

    function init() {
        //window.nekDebugSimulate = debugSimulate;

        startScreenObserver();
        startBlinkLoop();

        leader = new TabLeader('nek-arcade-leader', {
            onGainLeadership: () => {
                isLeader = true;
            },
            onLoseLeadership: () => {
                isLeader = false;
            },
        });
        leader.claim();
        startCreditCollector();

        if (typeof window.NekStyle?.registerExtension === 'function') {
            registerWithNekStyle();
            return;
        }
        let registered = false;
        window.__NekStyleExtensions = window.__NekStyleExtensions || [];
        window.__NekStyleExtensions.push(() => { registered = true; registerWithNekStyle(); });
        setTimeout(() => { if (!registered) injectStandaloneButton(); }, 0);
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();