From 45f94b1bdf8580cc6ea1802e0819003cda37a2ae Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Wed, 18 Mar 2026 02:37:01 +0100 Subject: [PATCH] init: Elden Ring death counter with multi-client server 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. --- .gitignore | 2 + CLAUDE.md | 21 +++ desktop.html | 473 +++++++++++++++++++++++++++++++++++++++++++++++++++ mobile.html | 261 ++++++++++++++++++++++++++++ obs.html | 121 +++++++++++++ server.ts | 84 +++++++++ 6 files changed, 962 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 desktop.html create mode 100644 mobile.html create mode 100644 obs.html create mode 100644 server.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c6364 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +counter.json +*.exe diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c355f6a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +A single-file, zero-dependency Elden Ring death counter (`Elden Ring Counter.html`). Open directly in any browser — no build step, no server needed. + +## Architecture + +Everything lives in one HTML file with three sections: + +- **CSS** (lines 8–339): Elden Ring-themed dark UI using CSS custom properties (`--gold`, `--bg0`, etc.). The "YOU DIED" overlay is CSS-animation-driven via the `.show` class. +- **HTML** (lines 342–376): Static markup. `#roman` and `#arabic` are the display targets; `#overlay` holds the death animation. +- **JS** (lines 378–468): Vanilla JS. `deaths` is the single state variable, persisted to `localStorage` under key `elden_deathcounter_value`. `render()` syncs state → DOM → storage. `add(delta)` is the only way to mutate count; it triggers the overlay on increment. + +## Key constraints + +- Roman numeral display is capped at 3999 (`clamp(0, 3999)`); 0 displays as `—`. +- The overlay animation restarts cleanly via a forced reflow (`void overlay.offsetWidth`) before re-adding `.show`. +- UI language is German (`lang="de"`); string literals like `"Tode:"` and `"Gespeichert lokal ✓"` are in German. diff --git a/desktop.html b/desktop.html new file mode 100644 index 0000000..185afe1 --- /dev/null +++ b/desktop.html @@ -0,0 +1,473 @@ + + + + + + Elden Deathcounter (Römisch) + + + + + +
+
+

Deathcounter

+

+ Persistenz: Server (synchronisiert über WebSocket)
+ Tasten: +/- oder ↑↓ • R Reset +

+
+ +
+
+
Tode: 0
+
+ +
+ + + +
+ +
+ Verbinde… + Römisch: 1–3999 (0 wird als — angezeigt) +
+
+ + + + + + + diff --git a/mobile.html b/mobile.html new file mode 100644 index 0000000..db924b6 --- /dev/null +++ b/mobile.html @@ -0,0 +1,261 @@ + + + + + + Elden Counter – Mobile + + + + + +

Deathcounter

+ +
+
+
Tode: 0
+
+ +
+
+ + +
+ +
+ + Verbinde… + + + + diff --git a/obs.html b/obs.html new file mode 100644 index 0000000..6472a82 --- /dev/null +++ b/obs.html @@ -0,0 +1,121 @@ + + + + + + Elden Counter – OBS + + + + + +
+
+
Tode: 0
+
+ + + + diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..91f7aee --- /dev/null +++ b/server.ts @@ -0,0 +1,84 @@ +import { join } from "jsr:@std/path"; + +const dir = import.meta.dirname!; + +let deaths = 0; +const clients = new Set(); + +// Load persisted count +try { + const data = JSON.parse(await Deno.readTextFile("counter.json")); + deaths = Number(data.count) || 0; +} catch { + // No file yet — start at 0 +} + +function broadcast(payload: string) { + for (const ws of clients) { + if (ws.readyState === WebSocket.OPEN) ws.send(payload); + } +} + +async function persist() { + await Deno.writeTextFile("counter.json", JSON.stringify({ count: deaths })); +} + +function handleSocket(ws: WebSocket) { + ws.onopen = () => { + clients.add(ws); + ws.send(JSON.stringify({ count: deaths })); + }; + + ws.onmessage = async (e) => { + try { + const msg = JSON.parse(e.data); + const before = deaths; + + if (msg.action === "increment") deaths = Math.min(3999, deaths + 1); + else if (msg.action === "decrement") deaths = Math.max(0, deaths - 1); + else if (msg.action === "reset") deaths = 0; + else return; + + if (deaths !== before) await persist(); + broadcast(JSON.stringify({ count: deaths })); + } catch { + // Ignore malformed messages + } + }; + + ws.onclose = () => clients.delete(ws); + ws.onerror = () => clients.delete(ws); +} + +async function handler(req: Request): Promise { + const url = new URL(req.url); + + if (url.pathname === "/ws") { + const upgrade = req.headers.get("upgrade") ?? ""; + if (upgrade.toLowerCase() !== "websocket") { + return new Response("WebSocket upgrade required", { status: 426 }); + } + const { socket, response } = Deno.upgradeWebSocket(req); + handleSocket(socket); + return response; + } + + if (url.pathname === "/mobile") { + const html = await Deno.readTextFile(join(dir, "mobile.html")); + return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); + } + + if (url.pathname === "/obs") { + const html = await Deno.readTextFile(join(dir, "obs.html")); + return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); + } + + // Default: desktop + const html = await Deno.readTextFile(join(dir, "desktop.html")); + return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } }); +} + +console.log("Elden Ring Counter running on http://localhost:8080"); +console.log("Mobile: http://:8080/mobile"); +console.log("Obs: http://:8080/obs"); +Deno.serve({ port: 8080 }, handler);