#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 {}