DeepCo NekHack

Mini game

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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