6bit.ch/html/js/qlpycon.js
2026-06-14 15:58:47 +02:00

263 lines
12 KiB
JavaScript

(function (global) {
var DEFAULTS = {
host: 'tcp://10.13.12.93:28960',
serverLabel: '[+] sexy Test',
mapName: 'campgrounds',
gameType: 'Clan Arena',
playerSlots: '6/9',
roundLimit: 9,
redTeam: ['Angel', 'Crash', 'Hunter'],
blueTeam: ['Mynx', 'Slash', 'Lucy'],
weapons: ['Railgun', 'Rocket Launcher', 'Plasma Gun', 'Lightning Gun', 'Shotgun'],
medals: ['IMPRESSIVE', 'EXCELLENT', 'REVENGE', 'MIDAIR'],
chatMessages: ['So much for foreplay...', 'We do not lose. We blend in.', 'The she-wolf reigns', 'Konnichiwa, campers', 'Not what I expected. Definitely not.', 'Just a flesh wound', 'Sayonara, babies', 'Oooooo! You like me. You really like me!', 'This is, like, so sweet', 'We are off to find the lizard', 'Do it again. But this time, with feeling.', 'Bizzzzaap!! Got me like a bug in a zap trap!', 'You flesh folk leak too much', 'Toodles', 'Great, premature eradication', '** winks and blows kisses **', 'Drinks are on me, boys!' ],
minDelay: 420,
maxDelay: 6900,
maxLines: 12,
pKill: 0.69,
pMedal: 0.1337,
spectators: [ 'anarki' ],
};
var colorizedNames = {
'anarki': '<span style="color:#f00">A</span><span style="color:#0f0">n</span><span style="color:#ff0">a</span><span style="color:#09f">r</span><span style="color:#0cc">k</span><span style="color:#f0f">i</span>'
};
function colorizeSpectator(name) {
return colorizedNames[name.toLowerCase()] || name;
}
function buildDoc(cfg) {
var redRows = cfg.redTeam .map(function(p){ return '<div class="player">0 ' + p + '</div>'; }).join('');
var blueRows = cfg.blueTeam.map(function(p){ return '<div class="player">0 ' + p + '</div>'; }).join('');
var label = cfg.serverLabel.split(' ');
var bracket = label[0] || '';
var cyan = label[1] || '';
var yellow = label.slice(2).join(' ') || '';
var css = [
'*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}',
'html,body{background:transparent;color:#ccc;font-family:"Courier New",Courier,monospace;font-size:12.5px;line-height:1.5;overflow:hidden}',
'.wrap{border:1px solid #333}',
'.header{padding:6px 12px 4px}',
'.titlebar{color:#ccc;margin-bottom:6px}',
'.infoline{display:grid;grid-template-columns:auto auto auto auto;margin-bottom:1px}',
'.teams{padding:8px 12px 4px;display:flex}',
'.team{width:50%}',
'.team-hdr{margin-bottom:3px}',
'.player{color:#fff;padding-left:2px}',
'.spectators{padding:4px 12px 2px;color:#ff0;font-weight:bold}',
'.divider{border:none;border-top:1px solid #444;margin:6px 0 0;display:block}',
'.feed-wrap{height:256px;overflow:hidden;padding:4px 12px;position:relative}',
'.fade{position:absolute;top:4px;left:0;right:0;height:30px;background:linear-gradient(to bottom,#0c0c0c,transparent);pointer-events:none;z-index:2}',
'.feed{display:flex;flex-direction:column}',
'.msg{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:0;transition:opacity .25s ease;line-height:1.6;display:block}',
'.msg.on{opacity:1}',
'.cmdline{border-top:1px solid #333;padding:4px 12px;display:flex;align-items:center;gap:4px;color:#ccc}',
'.cursor{display:inline-block;width:8px;height:14px;background:#ccc;vertical-align:middle;animation:blink 1s step-end infinite}',
'@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}',
'.cyan{color:#fff}.yellow{color:#0cc}.warmup{color:#f00}',
'.bold{color:#fff;font-weight:bold}',
'.rt{color:#f00;text-decoration:underline;font-weight:bold}',
'.bt{color:#09f;text-decoration:underline;font-weight:bold}',
'.score{color:#fff;font-weight:bold;margin-right:6px}',
'.ts{color:#ccc}.cr{color:#f00}.cb{color:#09f}.cp{color:#fff;font-weight:bold}',
'.cmt{color:#f0f;font-weight:bold}',
'.cst{color:#0c0;font-weight:bold}.css{color:#0f0}',
'.plain{color:#ccc}',
'.label{color:#ff0}',
].join('');
var html = [
'<!DOCTYPE html><html><head><meta charset="UTF-8">',
'<style>', css, '</style>',
'</head><body>',
'<div class="wrap">',
'<div class="header">',
'<div class="titlebar">Quake Live PyCon: ', cfg.host, '</div>',
'<div class="infoline">',
'<span><span class="label">Name: </span><span class="cr">[</span><span class="bold">+</span><span class="cr">]</span> <span class="cyan">', cyan, '</span> <span class="yellow">', yellow, '</span></span>',
'<span></span>',
'<span style="padding-left:5.7em"><span class="label">Time: </span><span class="bold" id="qtime">0:00</span></span>',
'<span style="padding-left:0.3em"><span class="label">Warmup: </span><span class="warmup">NO</span></span>',
'</div>',
'<div class="infoline">',
'<span><span class="label">Type: </span><span class="bold">', cfg.gameType, '</span></span>',
'<span><span class="label">Map: </span><span class="bold">', cfg.mapName, '</span></span>',
'<span><span class="label">Players: </span><span class="bold">', cfg.playerSlots, '</span></span>',
'<span><span class="label">Roundlimit: </span><span class="bold">', cfg.roundLimit, '</span></span>',
'</div>',
'</div>',
'<div class="teams">',
'<div class="team">',
'<div class="team-hdr"><span class="score" id="rs">0</span><span class="rt">RED TEAM</span></div>',
redRows,
'</div>',
'<div class="team">',
'<div class="team-hdr"><span class="score" id="bs">0</span><span class="bt">BLUE TEAM</span></div>',
blueRows,
'</div>',
'</div>',
'<div class="spectators">Spectators: <span class="cyan">' + cfg.spectators.map(colorizeSpectator).join(' ') + '</span></div>',
'<hr class="divider">',
'<div class="feed-wrap">',
'<div class="fade"></div>',
'<div class="feed" id="feed"></div>',
'</div>',
'<div class="cmdline">$ <span class="cursor"></span></div>',
'</div>',
].join('');
/* inner script as a real function — no string escaping needed */
function iframeScript(cfg) {
var feed = document.getElementById('feed');
var timeEl = document.getElementById('qtime');
var rsEl = document.getElementById('rs');
var bsEl = document.getElementById('bs');
var lines = [], elapsed = 0, scores = { red: 0, blue: 0 };
function rand(a) { return a[Math.floor(Math.random() * a.length)]; }
function randInt(a,b){ return a + Math.floor(Math.random() * (b - a + 1)); }
function sp(c, t) { return '<span class="' + c + '">' + t + '</span>'; }
function pad(n) { return n < 10 ? '0' + n : '' + n; }
function ts() {
var d = new Date();
return '<span style="color:#ff0">[</span>' + '<span style="color:#fff">' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()) + '</span>' + '<span style="color:#ff0">]</span>';
}
function kill() {
var rk = Math.random() < 0.5;
var k = rand(rk ? cfg.redTeam : cfg.blueTeam);
var v = rand(rk ? cfg.blueTeam : cfg.redTeam);
var w = rand(cfg.weapons);
var hp = randInt(25, 200);
if (Math.random() < 0.12) { if (rk) scores.red++; else scores.blue++; }
return sp('ts',ts()) + ' ' +
sp(rk?'cr':'cb', rk?'(RED)':'(BLUE)') + sp('cp', k) +
sp('plain', ' fragged ') +
sp(rk?'cb':'cr', rk?'(BLUE)':'(RED)') + sp('cp', v) +
sp('plain', ' with the ') +
(function(w){
var weaponColors = {
'Rocket Launcher':'#f00',
'Plasma Gun':'#f0f',
'Railgun':'#0f0',
'Lightning Gun':'#ff0',
'Shotgun':'#f60'
};
return '<span style="color:' + (weaponColors[w]||'#fff') + ';font-weight:bold">' + w + '</span>';
})(w) + ' ' +
sp('chp', '(' + hp + ' HP)');
}
function medal() {
var r = Math.random() < 0.5;
return sp('ts',ts()) + ' ' +
sp('cmt', '[MEDAL]') + ' ' +
sp(r?'cr':'cb', r?'(RED)':'(BLUE)') +
sp('cp', rand(r ? cfg.redTeam : cfg.blueTeam)) +
sp('plain', ' got ') +
(function(m){
var medalColors = {
'EXCELLENT':'#ff0',
'MIDAIR':'#0f0',
'IMPRESSIVE':'#0cc',
'REVENGE':'#f00'
};
return '<span style="color:' + (medalColors[m]||'#fff') + ';font-weight:bold">' + m + '</span>';
})(rand(cfg.medals));
}
function say() {
var r = Math.random() < 0.5;
return sp('ts',ts()) + ' ' +
sp('cst', '[SAY]') + ' ' +
sp(r?'cr':'cb', r?'(RED)':'(BLUE)') +
sp('cp', rand(r ? cfg.redTeam : cfg.blueTeam)) +
': ' + sp('css', rand(cfg.chatMessages));
}
function addLine(h) {
var d = document.createElement('div');
d.className = 'msg';
d.innerHTML = h;
feed.appendChild(d);
lines.push(d);
requestAnimationFrame(function(){ d.classList.add('on'); });
if (lines.length > cfg.maxLines) lines.shift().remove();
rsEl.textContent = scores.red;
bsEl.textContent = scores.blue;
}
function schedule() {
setTimeout(function() {
var r = Math.random();
if (r < cfg.pKill) addLine(kill());
else if (r < cfg.pKill + cfg.pMedal) addLine(medal());
else addLine(say());
schedule();
}, randInt(cfg.minDelay, cfg.maxDelay));
}
addLine(sp('plain', '*** '));
addLine(sp('plain', '*** QL PyCon Version 0.8.1 starting ***'));
addLine(sp('plain', 'Stats stream connected - ready for game events'));
setInterval(function() {
elapsed++;
var m = Math.floor(elapsed / 60), s = elapsed % 60;
timeEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
}, 1000);
setTimeout(schedule, 900);
}
var scriptBody = '(' + iframeScript.toString() + ')(' + JSON.stringify(cfg) + ')';
return html + '<scr' + 'ipt>' + scriptBody + '</scr' + 'ipt></body></html>';
}
var instanceCount = 0;
function mount(root, cfg) {
var iframe = document.createElement('iframe');
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
iframe.setAttribute('scrolling', 'no');
iframe.setAttribute('title', 'Quake Live terminal');
root.appendChild(iframe);
var doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(buildDoc(cfg));
doc.close();
}
global.QuakeTerminal = {
init: function(selector, userConfig) {
var root = typeof selector === 'string'
? document.querySelector(selector)
: selector;
if (!root) { console.warn('QuakeTerminal: element not found —', selector); return; }
var cfg = {};
for (var k in DEFAULTS) cfg[k] = DEFAULTS[k];
for (var k in (userConfig || {})) cfg[k] = userConfig[k];
cfg._id = ++instanceCount;
var details = root.closest('details');
if (details && !details.open) {
details.addEventListener('toggle', function onToggle() {
if (details.open) {
details.removeEventListener('toggle', onToggle);
mount(root, cfg);
}
});
} else {
mount(root, cfg);
}
},
defaults: DEFAULTS,
};
}(window));