DeepCo NekHack

Mini game

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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