helikon

Von $1

    Inhaltsverzeichnis
    keine Gliederung

    Version seit 19:08, 11 Aug 2026

    zu dieser Version.

    Zurück zu Versionshistorie.

    Zeige aktuelle Version

    #serverBuddy {
        position: fixed;
        display: none;
        right: 16px;
        top: 50%;
        transform: translateY(-50%);
        z-index: 2147483000;
        cursor: grab;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        -webkit-touch-callout: none;
        touch-action: none;
        opacity: 0.3;
        transition: opacity 0.5s ease;
    }
    #serverBuddy.active {
        opacity: 1;
    }
    #serverBuddy:hover {
        opacity: 1;
    }
    #serverBuddy.dragging {
        opacity: 1;
        cursor: grabbing;
        transition: none;
    }
    #serverBuddy.dizzy img {
        animation: buddyShake 0.15s ease-in-out 6;
    }
    #serverBuddy img {
        width: 96px;
        height: 96px;
        image-rendering: pixelated;
        image-rendering: crisp-edges;
        display: block;
        border: none;
        outline: none;
        pointer-events: none;
        -webkit-user-drag: none;
        user-drag: none;
        -webkit-touch-callout: none;
    }
    @keyframes buddyShake {
        0%   { transform: translate(0, 0) rotate(0deg); }
        25%  { transform: translate(-4px, 2px) rotate(-8deg); }
        50%  { transform: translate(4px, -2px) rotate(8deg); }
        75%  { transform: translate(-2px, 4px) rotate(-4deg); }
        100% { transform: translate(0, 0) rotate(0deg); }
    }
    #buddySpeech {
        position: fixed;
        bottom: 120px;
        left: 50%;
        transform: translateX(-50%) scale(0);
        background: #1a1a2e;
        color: #0df;
        padding: 8px 14px;
        border-radius: 10px;
        font-family: monospace;
        font-size: 11px;
        white-space: nowrap;
        z-index: 2147483001;
        opacity: 0;
        transition: opacity 0.3s ease, transform 0.3s ease;
        pointer-events: none;
        box-shadow: 0 2px 10px rgba(0,0,0,0.3);
    }
    #buddySpeech.visible {
        opacity: 1;
        transform: translateX(-50%) scale(1);
    }
    #buddySpeech::after {
        content: '';
        position: absolute;
        bottom: -8px;
        left: 50%;
        transform: translateX(-50%);
        width: 0;
        height: 0;
        border-left: 8px solid transparent;
        border-right: 8px solid transparent;
        border-top: 8px solid #1a1a2e;
    }
    
    (function() {
        /* === CONFIG === */
        var SLEEP_SEC     = 30;
        var HAPPY_SEC     = 4;
        var ERROR_SEC     = 6;
        var LOAD_SEC      = 5;
        var FADE_SEC      = 5;
        var TOPIC_DELAY   = 3;
        var DRAG_THRESHOLD = 8;
        var SHAKE_THRESHOLD = 4;
        var SHAKE_WINDOW   = 600;
        var SHAKE_MIN_DIST = 12;
        var DIZZY_SEC      = 4;
    
        /* === GIF URLS === */
        var GIFS = new Array(6);
        GIFS[0] = '/@api/deki/files/403/=buddy_idle.gif';
        GIFS[1] = '/@api/deki/files/402/=buddy_sleeping.gif';
        GIFS[2] = '/@api/deki/files/401/=buddy_happy.gif';
        GIFS[3] = '/@api/deki/files/400/=buddy_error.gif';
        GIFS[4] = '/@api/deki/files/399/=buddy_loading.gif';
        GIFS[5] = '/@api/deki/files/400/=buddy_error.gif';
    
        /* State indices */
        var S_IDLE  = 0;
        var S_SLEEP = 1;
        var S_HAPPY = 2;
        var S_ERROR = 3;
        var S_LOAD  = 4;
        var S_DIZZY = 5;
    
        /* === SPEECH MESSAGES === */
        var SPEECH = new Array(6);
        SPEECH[S_IDLE]  = new Array('Alles klar!', 'Bereit!', 'Was gibts?', 'Hm...');
        SPEECH[S_SLEEP] = new Array('zzz...', 'ZZzz...', '*schnarch*', 'zZzZz...');
        SPEECH[S_HAPPY] = new Array('Yay!', 'Super!', 'Nice!', 'Toll!', 'Weiter so!');
        SPEECH[S_ERROR] = new Array('Oops!', 'Fehler!', 'Aua!', 'Oh nein!');
        SPEECH[S_LOAD]  = new Array('Lade...', 'Moment...', 'Gleich...', 'Bitte warten...');
        SPEECH[S_DIZZY] = new Array('Ugh...', 'Schwindelig!', 'Stooop!', 'Mir wird schlecht!', 'Kopfweh!');
    
        /* === DOM REFS === */
        var bWrap  = document.getElementById('serverBuddy');
        var bImg   = document.getElementById('buddyImg');
        var bubble = document.getElementById('buddySpeech');
        if (!bWrap || !bImg || !bubble) return;
    
        /* === STATE === */
        var curState    = -1;
        var idleTimer   = null;
        var bubbleTimer = null;
        var leaving     = false;
        var preloading  = false;
        var pageError   = false;
        var pressTimer  = null;
    
        /* Drag state */
        var isDragging  = false;
        var wasDragged  = false;
        var dragStartX  = 0;
        var dragStartY  = 0;
        var dragOffsetX = 0;
        var dragOffsetY = 0;
    
        /* Shake detection */
        var shakeHistory = new Array();
        var lastDragY    = 0;
        var lastDragDir  = 0;
    
        /* === HELPERS === */
        function clamp(v, mn, mx) {
            if (v < mn) return mn;
            if (v > mx) return mx;
            return v;
        }
    
        function rnd(arr) {
            return arr[Math.floor(Math.random() * arr.length)];
        }
    
        function fetchText(url, cb) {
            var x = new XMLHttpRequest();
            x.open('GET', url, true);
            x.onreadystatechange = function() {
                if (x.readyState === 4) {
                    cb(x.status === 200 ? x.responseText : '');
                }
            };
            x.send(null);
        }
    
        /* === GIF / STATE === */
        function setGif(state) {
            if (state >= 0 && state < GIFS.length) {
                bImg.src = GIFS[state];
            }
        }
    
        function showBubble(text, ms) {
            if (bubbleTimer) clearTimeout(bubbleTimer);
            bubble.textContent = text;
            bubble.className = 'visible';
    
            /* Position bubble near buddy */
            var rect = bWrap.getBoundingClientRect();
            bubble.style.left   = (rect.left + 48) + 'px';
            bubble.style.bottom = (window.innerHeight - rect.top + 8) + 'px';
    
            if (ms) {
                bubbleTimer = setTimeout(function() {
                    hideBubble();
                }, ms);
            }
        }
    
        function hideBubble() {
            bubble.className = '';
            if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null; }
        }
    
        function setS(s, msg) {
            if (s === curState) return;
            curState = s;
            setGif(s);
    
            /* Dizzy class for CSS shake animation */
            var cls = bWrap.className || '';
            cls = cls.replace(/\bdizzy\b/g, '').replace(/\s+/g, ' ');
            if (s === S_DIZZY) {
                cls = cls + ' dizzy';
            }
            bWrap.className = cls;
    
            if (msg) {
                showBubble(msg, 4000);
            } else if (SPEECH[s]) {
                showBubble(rnd(SPEECH[s]), 4000);
            }
        }
    
        /* === IDLE / WAKE === */
        function resetIdle() {
            if (idleTimer) clearTimeout(idleTimer);
            if (curState === S_SLEEP) {
                wakeUp();
            }
            idleTimer = setTimeout(function() {
                if (!leaving && !preloading && !isDragging) {
                    setS(S_SLEEP);
                }
            }, SLEEP_SEC * 1000);
        }
    
        function wakeUp() {
            if (curState === S_SLEEP || curState === S_DIZZY) {
                curState = -1;
                setS(S_IDLE, 'Bin wach!');
            }
            bWrap.className = (bWrap.className || '').replace(/\bdizzy\b/g, '').replace(/\s+/g, ' ');
        }
    
        /* === POSITION SAVE/LOAD === */
        function savePosition(x, y) {
            try {
                localStorage.setItem('buddyPosX', String(Math.round(x)));
                localStorage.setItem('buddyPosY', String(Math.round(y)));
                localStorage.setItem('buddyCustomPos', '1');
            } catch(e) {}
        }
    
        function loadPosition() {
            try {
                var custom = localStorage.getItem('buddyCustomPos');
                if (custom === '1') {
                    var sx = localStorage.getItem('buddyPosX');
                    var sy = localStorage.getItem('buddyPosY');
                    if (sx !== null && sy !== null) {
                        var px = parseInt(sx, 10);
                        var py = parseInt(sy, 10);
                        var maxX = window.innerWidth - 96;
                        var maxY = window.innerHeight - 96;
                        px = clamp(px, 0, maxX);
                        py = clamp(py, 0, maxY);
                        applyPosition(px, py);
                        return true;
                    }
                }
            } catch(e) {}
            return false;
        }
    
        function applyPosition(x, y) {
            bWrap.style.right     = 'auto';
            bWrap.style.top       = 'auto';
            bWrap.style.bottom    = 'auto';
            bWrap.style.transform = 'none';
            bWrap.style.left      = x + 'px';
            bWrap.style.top       = y + 'px';
    
            /* Update bubble position */
            bubble.style.left   = (x + 48) + 'px';
            bubble.style.bottom = (window.innerHeight - y + 8) + 'px';
        }
    
        /* === ERROR DETECTION === */
        function checkErrors() {
            var hasErr = false;
            var imgs = document.getElementsByTagName('img');
            var i;
            for (i = 0; i < imgs.length; i++) {
                if (imgs[i] !== bImg && imgs[i].complete && imgs[i].naturalWidth === 0) {
                    hasErr = true;
                    break;
                }
            }
            if (hasErr && !pageError) {
                pageError = true;
                setS(S_ERROR, 'Bild-Fehler!');
            } else if (!hasErr && pageError) {
                pageError = false;
                setS(S_IDLE, 'Wieder OK!');
            }
        }
    
        /* === PRELOADER === */
        function isPreloaderVisible() {
            var pl = document.getElementById('preloader');
            if (!pl) return false;
            return pl.className.indexOf('show') !== -1;
        }
    
        function checkPreloader() {
            var plShow = isPreloaderVisible();
            if (plShow && !preloading) {
                preloading = true;
                wakeUp();
                curState = -1;
                setS(S_SLEEP, 'zzz...');
            } else if (!plShow && preloading) {
                preloading = false;
                curState = -1;
                setS(S_LOAD, 'Lade Seite...');
                setTimeout(function() {
                    leaving = false;
                    setS(S_IDLE, 'Bereit!');
                    setTimeout(function() { fetchPageTopic(); }, TOPIC_DELAY * 1000);
                }, 2000);
            }
        }
    
        /* === PAGE TOPIC === */
        function fetchPageTopic() {
            var h1 = document.querySelector('h1');
            if (h1 && h1.textContent) {
                var title = h1.textContent.replace(/^\s+|\s+$/g, '');
                if (title.length > 0 && title.length < 60) {
                    showBubble(title, 5000);
                }
            }
        }
    
        /* === CHAT LINK === */
        function goToChat() {
            var chatLink = document.querySelector('a[href*="Helikon"]');
            if (chatLink) {
                window.location.href = chatLink.href;
            } else {
                window.location.href = '/Helikon';
            }
        }
    
        /* === DRAG AND DROP === */
        function getEventXY(e) {
            if (e.touches && e.touches.length > 0) {
                return { x: e.touches[0].clientX, y: e.touches[0].clientY };
            }
            return { x: e.clientX, y: e.clientY };
        }
    
        function onDragStart(e) {
            if (e.button && e.button !== 0) return;
            var pt = getEventXY(e);
            var rect = bWrap.getBoundingClientRect();
            dragStartX  = pt.x;
            dragStartY  = pt.y;
            dragOffsetX = pt.x - rect.left;
            dragOffsetY = pt.y - rect.top;
            isDragging  = false;
            wasDragged  = false;
    
            /* reset shake tracking */
            shakeHistory = new Array();
            lastDragY    = pt.y;
            lastDragDir  = 0;
        }
    
        function onDragMove(e) {
            if (dragOffsetX === 0 && dragOffsetY === 0) return;
            var pt = getEventXY(e);
            var dx = Math.abs(pt.x - dragStartX);
            var dy = Math.abs(pt.y - dragStartY);
    
            if (!isDragging && (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD)) {
                isDragging = true;
                wasDragged = true;
                var cls = bWrap.className || '';
                cls = cls.replace(/\bdragging\b/g, '');
                bWrap.className = cls + ' dragging active';
                hideBubble();
            }
    
            if (!isDragging) return;
    
            /* CRITICAL: prevent scroll on mobile */
            if (e.preventDefault) e.preventDefault();
            if (e.stopPropagation) e.stopPropagation();
    
            var maxX = window.innerWidth - 96;
            var maxY = window.innerHeight - 96;
            var nx = clamp(pt.x - dragOffsetX, 0, maxX);
            var ny = clamp(pt.y - dragOffsetY, 0, maxY);
    
            applyPosition(nx, ny);
    
            /* === SHAKE DETECTION (Y-axis direction changes) === */
            var deltaY = pt.y - lastDragY;
            if (Math.abs(deltaY) > SHAKE_MIN_DIST) {
                var dir = deltaY > 0 ? 1 : -1;
                if (lastDragDir !== 0 && dir !== lastDragDir) {
                    shakeHistory.push(Date.now());
                }
                lastDragDir = dir;
                lastDragY = pt.y;
            }
    
            /* clean old entries */
            var now = Date.now();
            var cleaned = new Array();
            var si;
            for (si = 0; si < shakeHistory.length; si++) {
                if (now - shakeHistory[si] < SHAKE_WINDOW) {
                    cleaned.push(shakeHistory[si]);
                }
            }
            shakeHistory = cleaned;
    
            if (shakeHistory.length >= SHAKE_THRESHOLD) {
                shakeHistory = new Array();
                if (curState !== S_DIZZY) {
                    curState = -1;
                    setS(S_DIZZY);
                    /* Auto-recover after dizzy */
                    setTimeout(function() {
                        if (curState === S_DIZZY) {
                            curState = -1;
                            setS(S_IDLE, 'Uff... besser.');
                        }
                    }, DIZZY_SEC * 1000);
                }
            }
        }
    
        function onDragEnd(e) {
            if (isDragging) {
                var rect = bWrap.getBoundingClientRect();
                savePosition(rect.left, rect.top);
            }
            var cls = bWrap.className || '';
            cls = cls.replace(/\bdragging\b/g, '').replace(/\s+/g, ' ');
            bWrap.className = cls;
            isDragging  = false;
            dragOffsetX = 0;
            dragOffsetY = 0;
            shakeHistory = new Array();
            lastDragDir  = 0;
        }
    
        /* === EVENT BINDING === */
        function bindEvents() {
            /* --- MOUSE --- */
            bWrap.addEventListener('mousedown', function(e) {
                e.preventDefault();
                onDragStart(e);
                pressTimer = setTimeout(function() {
                    if (!wasDragged) goToChat();
                }, 800);
            });
    
            document.addEventListener('mousemove', function(e) {
                onDragMove(e);
            });
    
            document.addEventListener('mouseup', function(e) {
                if (pressTimer) clearTimeout(pressTimer);
                /* Click detection: short press + no drag = click */
                if (!wasDragged && !isDragging) {
                    /* Show happy + speech on click */
                    curState = -1;
                    setS(S_HAPPY);
                    setTimeout(function() {
                        if (curState === S_HAPPY) {
                            curState = -1;
                            setS(S_IDLE);
                        }
                    }, HAPPY_SEC * 1000);
                }
                onDragEnd(e);
            });
    
            /* --- TOUCH (mobile) --- */
            var touchOpts = false;
            try {
                var testOpts = Object.defineProperty(new Object(), 'passive', {
                    get: function() { touchOpts = true; return false; }
                });
                window.addEventListener('__test__', null, testOpts);
                window.removeEventListener('__test__', null, testOpts);
            } catch(e) {
                touchOpts = false;
            }
    
            var passiveFalse = touchOpts ? { passive: false, capture: false } : false;
    
            bWrap.addEventListener('touchstart', function(e) {
                e.preventDefault();
                onDragStart(e);
                pressTimer = setTimeout(function() {
                    if (!wasDragged) {
                        goToChat();
                    }
                }, 800);
            }, passiveFalse);
    
            document.addEventListener('touchmove', function(e) {
                if (isDragging || (dragOffsetX !== 0 || dragOffsetY !== 0)) {
                    onDragMove(e);
                }
            }, passiveFalse);
    
            document.addEventListener('touchend', function(e) {
                if (pressTimer) clearTimeout(pressTimer);
                /* Tap detection on mobile: short + no drag */
                if (!wasDragged && !isDragging) {
                    curState = -1;
                    setS(S_HAPPY);
                    setTimeout(function() {
                        if (curState === S_HAPPY) {
                            curState = -1;
                            setS(S_IDLE);
                        }
                    }, HAPPY_SEC * 1000);
                }
                onDragEnd(e);
            });
    
            /* Cancel long press if finger moves */
            bWrap.addEventListener('touchmove', function() {
                if (pressTimer) clearTimeout(pressTimer);
            }, passiveFalse);
    
            /* --- IDLE TRACKERS --- */
            document.addEventListener('mousemove', resetIdle);
            document.addEventListener('scroll', resetIdle);
            document.addEventListener('keydown', resetIdle);
            document.addEventListener('touchstart', resetIdle);
    
            bWrap.addEventListener('mouseenter', function() {
                wakeUp();
                bWrap.className = (bWrap.className || '').replace(/\bactive\b/g, '') + ' active';
            });
            bWrap.addEventListener('mouseleave', function() {
                if (pressTimer) clearTimeout(pressTimer);
                if (!isDragging) {
                    var cls = bWrap.className || '';
                    bWrap.className = cls.replace(/\bactive\b/g, '').replace(/\s+/g, ' ');
                }
            });
    
            /* --- LINK CLICKS (navigation detection) --- */
            document.addEventListener('click', function(e) {
                resetIdle();
                var t = e.target;
                var link = null;
                if (t.tagName === 'A') {
                    link = t;
                } else if (t.parentNode && t.parentNode.tagName === 'A') {
                    link = t.parentNode;
                }
                if (link && link.href) {
                    var href = link.href;
                    var loc  = window.location;
                    var isExternal = href.indexOf(loc.hostname) === -1;
                    var HASH = String.fromCharCode(35);
                    var isSamePage = href.indexOf(HASH) !== -1 && href.split(HASH)[0] === loc.href.split(HASH)[0];
                    if (!isSamePage) {
                        leaving = true;
                        if (isExternal) {
                            setS(S_HAPPY, 'Tschuess!');
                        } else {
                            setS(S_LOAD, 'Lade...');
                        }
                    }
                }
            });
        }
    
        /* === INIT === */
        function init() {
            bWrap.style.display = 'block';
            setGif(S_LOAD);
    
            /* Load saved position or use default */
            if (!loadPosition()) {
                /* Default: right side, vertically centered */
                var defX = window.innerWidth - 112;
                var defY = Math.round(window.innerHeight / 2) - 48;
                applyPosition(defX, defY);
            }
    
            bindEvents();
    
            /* Initial state */
            setTimeout(function() {
                bWrap.className = (bWrap.className || '') + ' active';
                setS(S_IDLE, 'Hallo!');
                setTimeout(function() {
                    /* Fade to transparent after intro */
                    var cls = bWrap.className || '';
                    if (cls.indexOf('dragging') === -1) {
                        bWrap.className = cls.replace(/\bactive\b/g, '').replace(/\s+/g, ' ');
                    }
                }, FADE_SEC * 1000);
            }, LOAD_SEC * 1000);
    
            /* Start idle timer */
            resetIdle();
    
            /* Periodic checks */
            setInterval(checkErrors, 10000);
            setInterval(checkPreloader, 1000);
    
            /* Fetch page topic after delay */
            setTimeout(function() { fetchPageTopic(); }, TOPIC_DELAY * 1000);
        }
    
        /* GO */
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            init();
        } else {
            document.addEventListener('DOMContentLoaded', init, false);
        }
    
    })();
    
    NoElements {}

     
    Use the language selection below for English Utilise le sélecteur de langue ci-dessous pour français Per l'italiano, utilizzare la selezione della lingua qui sotto Schön sprichst du deutsch :-)