Deno HTTP + WebSocket server serving three pages: - / desktop with YOU DIED overlay and keyboard controls - /mobile touch-optimized control page - /obs transparent browser source for OBS Count persisted to counter.json, synced in real time across all connected clients. Compiles to a self-contained Windows .exe via deno compile.
262 lines
6.9 KiB
HTML
262 lines
6.9 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||
<title>Elden Counter – Mobile</title>
|
||
|
||
<style>
|
||
:root{
|
||
--bg0:#06060a;
|
||
--bg1:#0b0b12;
|
||
--panel: rgba(12, 12, 18, .85);
|
||
--border: rgba(212, 175, 55, .30);
|
||
--border2: rgba(212, 175, 55, .15);
|
||
--text: rgba(245, 238, 210, .92);
|
||
--muted: rgba(245, 238, 210, .62);
|
||
--gold: rgba(212, 175, 55, .95);
|
||
--shadow: 0 26px 80px rgba(0,0,0,.55);
|
||
--radius: 18px;
|
||
}
|
||
|
||
*{ box-sizing:border-box; -webkit-tap-highlight-color: transparent; }
|
||
|
||
html, body{
|
||
margin:0;
|
||
height:100%;
|
||
}
|
||
|
||
body{
|
||
min-height:100svh;
|
||
display:flex;
|
||
flex-direction:column;
|
||
align-items:center;
|
||
justify-content:center;
|
||
gap:20px;
|
||
padding:24px 16px;
|
||
color:var(--text);
|
||
font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
|
||
background:
|
||
radial-gradient(1200px 650px at 50% 18%, rgba(212,175,55,.10), transparent 55%),
|
||
radial-gradient(900px 500px at 20% 70%, rgba(180,120,40,.08), transparent 60%),
|
||
linear-gradient(180deg, var(--bg1), var(--bg0));
|
||
overflow:hidden;
|
||
}
|
||
|
||
body::before{
|
||
content:"";
|
||
position:fixed;
|
||
inset:0;
|
||
pointer-events:none;
|
||
background-image: radial-gradient(rgba(255,255,255,.06) 1px, transparent 1px);
|
||
background-size: 3px 3px;
|
||
opacity:.08;
|
||
mix-blend-mode: overlay;
|
||
filter: blur(.2px);
|
||
}
|
||
|
||
h1{
|
||
margin:0;
|
||
font-size: 15px;
|
||
letter-spacing: 1.4px;
|
||
font-weight: 800;
|
||
color: rgba(245,238,210,.70);
|
||
text-transform: uppercase;
|
||
font-variant: small-caps;
|
||
position:relative;
|
||
z-index:1;
|
||
}
|
||
|
||
.bigBox{
|
||
position:relative;
|
||
z-index:1;
|
||
width:min(360px, 90vw);
|
||
border:1px solid var(--border);
|
||
border-radius: 20px;
|
||
padding: 28px 16px 22px;
|
||
display:grid;
|
||
place-items:center;
|
||
background:
|
||
radial-gradient(500px 220px at 50% 30%, rgba(212,175,55,.12), transparent 70%),
|
||
linear-gradient(180deg, rgba(255,255,255,.04), rgba(0,0,0,.12));
|
||
box-shadow: inset 0 0 0 1px rgba(0,0,0,.35), var(--shadow);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.roman{
|
||
font-size: clamp(52px, 14vw, 80px);
|
||
font-weight: 900;
|
||
letter-spacing: 4px;
|
||
color: rgba(245,238,210,.94);
|
||
text-shadow:
|
||
0 0 16px rgba(212,175,55,.12),
|
||
0 0 34px rgba(212,175,55,.08);
|
||
text-align:center;
|
||
font-variant: small-caps;
|
||
}
|
||
|
||
.arabic{
|
||
margin-top:8px;
|
||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||
font-size: 14px;
|
||
color: var(--muted);
|
||
letter-spacing:.2px;
|
||
}
|
||
|
||
.controls{
|
||
position:relative;
|
||
z-index:1;
|
||
display:flex;
|
||
flex-direction:column;
|
||
gap:14px;
|
||
width:min(360px, 90vw);
|
||
}
|
||
|
||
.row{
|
||
display:flex;
|
||
gap:14px;
|
||
}
|
||
|
||
button{
|
||
appearance:none;
|
||
flex:1;
|
||
border:1px solid var(--border);
|
||
color: var(--text);
|
||
background: linear-gradient(180deg, rgba(212,175,55,.16), rgba(0,0,0,.18));
|
||
border-radius: 16px;
|
||
font-size: 26px;
|
||
font-weight: 900;
|
||
letter-spacing:.4px;
|
||
cursor:pointer;
|
||
box-shadow: 0 10px 22px rgba(0,0,0,.35);
|
||
transition: transform .08s ease, filter .12s ease, border-color .12s ease;
|
||
font-variant: small-caps;
|
||
font-family: ui-serif, Georgia, "Times New Roman", Times, serif;
|
||
padding: 22px 10px;
|
||
/* Prevent double-tap zoom */
|
||
touch-action: manipulation;
|
||
}
|
||
|
||
button:active{
|
||
transform: scale(0.97);
|
||
filter: brightness(1.12);
|
||
border-color: rgba(212,175,55,.55);
|
||
}
|
||
|
||
.btnGhost{
|
||
border-color: rgba(245,238,210,.18);
|
||
background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(0,0,0,.18));
|
||
font-size: 18px;
|
||
}
|
||
|
||
.status{
|
||
position:relative;
|
||
z-index:1;
|
||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||
font-size: 12px;
|
||
color: var(--muted);
|
||
border:1px solid rgba(245,238,210,.14);
|
||
border-radius:999px;
|
||
padding:6px 14px;
|
||
background: rgba(255,255,255,.03);
|
||
transition: border-color .3s ease;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>Deathcounter</h1>
|
||
|
||
<section class="bigBox" aria-live="polite">
|
||
<div class="roman" id="roman">—</div>
|
||
<div class="arabic" id="arabic">Tode: 0</div>
|
||
</section>
|
||
|
||
<div class="controls">
|
||
<div class="row">
|
||
<button id="minus">−1</button>
|
||
<button id="plus">+1</button>
|
||
</div>
|
||
<button class="btnGhost" id="reset">Reset</button>
|
||
</div>
|
||
|
||
<span class="status" id="status">Verbinde…</span>
|
||
|
||
<script>
|
||
function toRoman(n) {
|
||
const map = [
|
||
{ val: 1000, sym: "M" },
|
||
{ val: 900, sym: "CM" },
|
||
{ val: 500, sym: "D" },
|
||
{ val: 400, sym: "CD" },
|
||
{ val: 100, sym: "C" },
|
||
{ val: 90, sym: "XC" },
|
||
{ val: 50, sym: "L" },
|
||
{ val: 40, sym: "XL" },
|
||
{ val: 10, sym: "X" },
|
||
{ val: 9, sym: "IX" },
|
||
{ val: 5, sym: "V" },
|
||
{ val: 4, sym: "IV" },
|
||
{ val: 1, sym: "I" }
|
||
];
|
||
let res = "";
|
||
for (const { val, sym } of map) {
|
||
while (n >= val) { res += sym; n -= val; }
|
||
}
|
||
return res;
|
||
}
|
||
|
||
const romanEl = document.getElementById("roman");
|
||
const arabicEl = document.getElementById("arabic");
|
||
const statusEl = document.getElementById("status");
|
||
|
||
let currentCount = 0;
|
||
|
||
function clamp(n, min, max){ return Math.max(min, Math.min(max, n)); }
|
||
|
||
function render(count) {
|
||
currentCount = clamp(count, 0, 3999);
|
||
romanEl.textContent = currentCount === 0 ? "—" : toRoman(currentCount);
|
||
arabicEl.textContent = `Tode: ${currentCount}`;
|
||
}
|
||
|
||
let ws;
|
||
|
||
function connect() {
|
||
ws = new WebSocket(`ws://${location.host}/ws`);
|
||
|
||
ws.onopen = () => {
|
||
statusEl.textContent = "Verbunden ✓";
|
||
statusEl.style.borderColor = "rgba(212,175,55,.35)";
|
||
setTimeout(() => (statusEl.style.borderColor = "rgba(245,238,210,.14)"), 1500);
|
||
};
|
||
|
||
ws.onmessage = (e) => {
|
||
const msg = JSON.parse(e.data);
|
||
if (typeof msg.count === "number") render(msg.count);
|
||
};
|
||
|
||
ws.onclose = () => {
|
||
statusEl.textContent = "Getrennt — verbinde neu…";
|
||
statusEl.style.borderColor = "rgba(170,30,25,.5)";
|
||
setTimeout(connect, 2000);
|
||
};
|
||
|
||
ws.onerror = () => ws.close();
|
||
}
|
||
|
||
function send(action) {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({ action }));
|
||
}
|
||
}
|
||
|
||
document.getElementById("plus").addEventListener("click", () => send("increment"));
|
||
document.getElementById("minus").addEventListener("click", () => send("decrement"));
|
||
document.getElementById("reset").addEventListener("click", () => send("reset"));
|
||
|
||
connect();
|
||
</script>
|
||
</body>
|
||
</html>
|