Files
emmatherock 5a2c3272e9 Translate remaining Go source and overlay.html comments to English
Identifiers, comments, and log/console messages in auth.go, i18n.go,
counter.go, totals.go, and overlay.html's script/CSS comments — the
files the TLS commit didn't already touch. No functional changes.

locales/*.json and overlay.html's own FALLBACK/data-i18n strings stay
bilingual on purpose: that's end-user text for the overlay/panel, a
different audience than the source code, served by the existing i18n
system rather than this convention.
2026-09-17 21:51:33 -03:00

380 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Elden Ring Death Counter</title>
<style>
:root{
--bg:#0b0906;
--bg-elevated:#17130f;
--bg-card:#1d1712;
--gold:#c9a24b;
--gold-bright:#e8c874;
--ash-red:#8a2a2a;
--ash-red-bright:#b23a3a;
--parchment:#e9e2cf;
--muted:#948a76;
--rule:#3a3226;
--ok:#7fae6e;
--shadow:0 20px 60px rgba(0,0,0,.55);
}
*{box-sizing:border-box;}
html,body{background:var(--bg);}
body{
margin:0; min-height:100vh; background:var(--bg); color:var(--parchment);
font-family:'EB Garamond', Georgia, 'Times New Roman', serif;
-webkit-font-smoothing:antialiased;
}
[hidden]{display:none!important;}
.display,h1,.num{font-family:'Cinzel', Georgia, serif;}
.eyebrow{font-family:'Cinzel', Georgia, serif;font-size:.72rem;letter-spacing:.32em;text-transform:uppercase;color:var(--muted);}
.num{font-variant-numeric:tabular-nums;font-feature-settings:"tnum";}
@media (prefers-reduced-motion: reduce){*{animation-duration:.001ms!important;transition-duration:.001ms!important;}}
/* overlay (OBS) */
html.mode-overlay,html.mode-overlay body{background:transparent;}
#overlay{background:transparent;min-height:100vh;display:flex;align-items:flex-start;justify-content:flex-start;padding:16px;}
.plate{
background:rgba(9,7,5,.9); backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
border-top:1px solid var(--gold); border-bottom:1px solid var(--gold);
padding:18px 24px; width:600px; box-sizing:border-box; box-shadow:var(--shadow);
}
/* With a single player the plate narrows; the OBS canvas doesn't
change, it just takes up less of it. So the dimensions get set once. */
.plate.solo{width:420px;}
.players{display:flex;align-items:stretch;justify-content:center;}
.pcol{flex:1 1 0;min-width:0;text-align:center;}
.pname{
font-family:'Cinzel', Georgia, serif;font-size:.78rem;letter-spacing:.24em;text-transform:uppercase;
color:var(--gold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.pnum{
display:block;font-family:'Cinzel', Georgia, serif;font-variant-numeric:tabular-nums;
font-size:3.6rem;line-height:1.05;font-weight:600;color:var(--parchment);margin-top:.25rem;
transition:color .15s ease, transform .15s ease;
}
.pnum.pulse{color:var(--gold-bright);transform:scale(1.06);}
/* A single line under the number: says "deaths" and, when there's
something else to report (boss, no connection), appends it right
there. That way the word is always present and the overlay's height
never changes. */
.pnote{
font-family:'Cinzel', Georgia, serif;font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;
color:var(--muted);min-height:1.5em;margin-top:.35rem;
}
.pnote .st{color:var(--ash-red-bright);font-style:italic;}
.pnote .sep{opacity:.5;margin:0 .45em;}
.pcol.off .pname,.pcol.off .pnum{color:var(--muted);}
.pdiv{width:1px;flex:0 0 1px;margin:0 20px;background:linear-gradient(180deg,transparent,var(--gold),transparent);opacity:.75;}
/* control / status */
#control{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:3rem 1.25rem;}
.card{width:100%;max-width:560px;background:var(--bg-card);border:1px solid var(--rule);box-shadow:var(--shadow);padding:2.4rem 2.4rem 2rem;position:relative;}
.card::before,.card::after{content:"";position:absolute;left:2.4rem;right:2.4rem;height:1px;background:linear-gradient(90deg,transparent,var(--gold),transparent);opacity:.6;}
.card::before{top:1.35rem;} .card::after{bottom:1.35rem;}
.title{text-align:center;font-size:1.05rem;letter-spacing:.28em;text-transform:uppercase;color:var(--gold);margin:0 0 .3rem;}
.title small{display:block;font-family:'EB Garamond', Georgia, serif;letter-spacing:0;text-transform:none;font-style:italic;color:var(--muted);font-size:.95rem;margin-top:.3rem;}
.hero{text-align:center;margin:2.1rem 0 1.6rem;}
.hero .num{font-size:6rem;line-height:1;font-weight:700;color:var(--parchment);transition:color .15s ease, transform .15s ease;}
.hero .num.pulse{color:var(--gold-bright);transform:scale(1.05);}
.hero .eyebrow{margin-top:.5rem;}
.stats{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--rule);margin:1.6rem 0;border:1px solid var(--rule);}
.stat{background:var(--bg-elevated);padding:.9rem 1rem;text-align:center;}
.stat .stat-num{font-size:1.5rem;font-weight:600;color:var(--gold);}
.stat .stat-label{font-size:.65rem;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-top:.2rem;}
.field{margin:1.4rem 0;}
.field label{display:block;font-size:.7rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-bottom:.4rem;}
.field .row{display:flex;gap:.5rem;}
.field input[type=number],.field input[type=text]{
width:100%;background:var(--bg-elevated);border:1px solid var(--rule);color:var(--parchment);
font-family:'EB Garamond', Georgia, serif;font-size:1.05rem;padding:.55rem .75rem;
}
.field input:focus{border-color:var(--gold);outline:none;}
.btn{border:1px solid var(--gold);background:transparent;color:var(--gold-bright);padding:.6rem 1rem;font-size:.85rem;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;transition:background .15s ease;}
.btn:hover{background:rgba(201,162,75,.12);}
.hint{font-size:.78rem;color:var(--muted);margin-top:.5rem;line-height:1.5;}
.obs-box{margin-top:1.8rem;padding-top:1.3rem;border-top:1px solid var(--rule);}
.obs-url-row{display:flex;gap:.5rem;}
.obs-url-row input{flex:1;background:var(--bg-elevated);border:1px solid var(--rule);color:var(--muted);font-family:'EB Garamond', monospace;font-size:.85rem;padding:.5rem .6rem;}
.copy-btn{border:1px solid var(--rule);background:transparent;color:var(--muted);padding:.5rem .9rem;font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;cursor:pointer;}
.copy-btn:hover{border-color:var(--gold);color:var(--gold);}
.status{text-align:center;font-size:.75rem;color:var(--muted);margin-top:1.3rem;letter-spacing:.05em;}
.status .dot{display:inline-block;width:.45em;height:.45em;border-radius:50%;background:var(--muted);margin-right:.4em;}
.status.live .dot{background:var(--ok);}
.status.err .dot{background:var(--ash-red-bright);}
.build{text-align:center;font-size:.68rem;color:var(--muted);margin-top:.4rem;letter-spacing:.04em;opacity:.7;}
.boss-flag{display:none;text-align:center;color:var(--ash-red-bright);font-style:italic;letter-spacing:.08em;margin-top:.3rem;}
.boss-flag.on{display:block;}
.peer-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;padding:.45rem .75rem;background:var(--bg-elevated);border:1px solid var(--rule);margin-bottom:.35rem;}
.peer-row .pn{color:var(--parchment);}
.peer-row .pv{font-family:'Cinzel', Georgia, serif;color:var(--gold);font-variant-numeric:tabular-nums;}
.peer-row.off .pn,.peer-row.off .pv{color:var(--muted);}
.peer-row .ps{font-size:.72rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);}
</style>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600;700&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
<script>
(function(){
var params = new URLSearchParams(location.search);
var mode = (params.get('view') || 'control').toLowerCase();
if (mode !== 'overlay') mode = 'control';
document.documentElement.classList.add('mode-' + mode);
window.__mode = mode;
})();
</script>
</head>
<body>
<div id="overlay" hidden>
<div class="plate solo" id="ov-plate">
<div class="players" id="ov-players"></div>
</div>
</div>
<div id="control" hidden>
<div class="card">
<p class="title"><span data-i18n="panel.title">Elden Ring</span><small data-i18n="panel.subtitle">Death Counter</small></p>
<div class="hero">
<span class="num" id="c-total">–</span>
<div class="eyebrow" data-i18n="panel.total">Total deaths</div>
</div>
<div class="boss-flag" id="c-boss" data-i18n="panel.boss">⚔ In a boss fight</div>
<div class="stats">
<div class="stat"><div class="stat-num" id="c-raw">–</div><div class="stat-label" data-i18n="panel.raw">Read from process</div></div>
<div class="stat"><div class="stat-num" id="c-slot">–</div><div class="stat-label" data-i18n="panel.slot">Save slot</div></div>
</div>
<div class="field" id="c-peers-box" hidden>
<label data-i18n="panel.partners">Partners</label>
<div id="c-peers"></div>
</div>
<div class="obs-box">
<div class="field">
<label data-i18n="panel.obsUrl">URL for OBS (Browser Source)</label>
<div class="obs-url-row">
<input type="text" id="obs-url" readonly>
<button class="copy-btn" id="copy-btn" data-i18n="panel.copy">Copy</button>
</div>
</div>
<p class="hint" data-i18n="panel.obsHint"></p>
</div>
<p class="status" id="status"><span class="dot"></span><span id="status-text" data-i18n="status.connecting">Connecting…</span></p>
<p class="build" id="c-build"></p>
</div>
</div>
<script>
(function(){
var mode = window.__mode || 'control';
// Interface text. The real dictionary comes from /strings.json (see
// i18n.go and locales/*.json). This one here is the lifeboat: if that
// request fails, you see English and never a raw key.
var FALLBACK = {
"overlay.deaths": "deaths",
"overlay.boss": "boss",
"overlay.offline": "no connection",
"overlay.waiting": "waiting for the game…",
"overlay.you": "You",
"overlay.partner": "Partner",
"panel.title": "Elden Ring",
"panel.subtitle": "Death Counter · live reading",
"panel.total": "Total deaths",
"panel.raw": "Read from process",
"panel.slot": "Save slot",
"panel.boss": "⚔ In a boss fight",
"panel.partners": "Partners",
"panel.obsUrl": "URL for OBS (Browser Source)",
"panel.copy": "Copy",
"panel.copied": "Copied",
"panel.copyManual": "Select and copy",
"panel.obsHint": "Paste this URL into a Browser source in OBS. Transparent background, updates on its own as long as this console window stays open.",
"panel.build": "Build",
"status.connecting": "Connecting…",
"status.waitingGame": "Waiting for you to open Elden Ring…",
"status.menu": "Elden Ring open — main menu or loading screen",
"status.live": "Live — reading Elden Ring",
"status.noServer": "No connection to the local program",
"peer.playing": "playing",
"peer.menu": "in the menu",
"peer.offline": "no connection"
};
var STR = {};
function t(k){ return STR[k] || FALLBACK[k] || k; }
function applyStrings(){
var nodes = document.querySelectorAll('[data-i18n]');
for (var i = 0; i < nodes.length; i++){
nodes[i].textContent = t(nodes[i].getAttribute('data-i18n'));
}
document.documentElement.lang = STR['_lang'] || 'en';
}
document.getElementById(mode === 'overlay' ? 'overlay' : 'control').hidden = false;
function fmtElapsed(ms){
if (ms == null) return '—';
var s = Math.floor(ms/1000);
var h = Math.floor(s/3600), m = Math.floor((s%3600)/60), ss = s%60;
function pad(n){return (n<10?'0':'')+n;}
return h>0 ? (h+':'+pad(m)+':'+pad(ss)) : (pad(m)+':'+pad(ss));
}
var prevTotal = null;
function pulse(el){ el.classList.add('pulse'); setTimeout(function(){el.classList.remove('pulse');},260); }
if (mode === 'control'){
document.getElementById('obs-url').value = location.origin + '/?view=overlay';
document.getElementById('copy-btn').addEventListener('click', function(){
var btn=this, url=document.getElementById('obs-url');
function done(ok){ btn.textContent = ok?t('panel.copied'):t('panel.copyManual'); setTimeout(function(){btn.textContent=t('panel.copy');},1400); }
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(url.value).then(function(){done(true);}, function(){url.select();done(false);});
} else { url.select(); done(false); }
});
}
function setStatus(text, cls){
var el = document.getElementById('status');
if (!el) return;
document.getElementById('status-text').textContent = text;
el.className = 'status' + (cls ? ' ' + cls : '');
}
// Column cache: rebuilding the DOM on every tick would make the
// overlay flicker and lose the pulse animation.
//
// Indexed by POSITION, never by name. Indexing by name was a real bug:
// switching characters changed the key, spawning a new column and
// leaving the old one stuck in the DOM, so the same player showed up
// twice. The name is content, not identity.
var slots = [];
var prevByName = {};
// ensureSlots leaves exactly n columns, creating or removing whatever's
// needed (with its divider).
function ensureSlots(list, n){
while (slots.length > n){
var old = slots.pop();
if (old.div) list.removeChild(old.div);
list.removeChild(old.col);
}
while (slots.length < n){
var i = slots.length;
var div = null;
if (i > 0){
div = document.createElement('div');
div.className = 'pdiv';
list.appendChild(div);
}
var col = document.createElement('div'); col.className = 'pcol';
var nm = document.createElement('div'); nm.className = 'pname';
var vl = document.createElement('span'); vl.className = 'pnum';
var nt = document.createElement('div'); nt.className = 'pnote';
col.appendChild(nm); col.appendChild(vl); col.appendChild(nt);
list.appendChild(col);
slots.push({col:col, div:div, name:nm, num:vl, note:nt});
}
}
function playersOf(d){
if (d.players && d.players.length) return d.players;
// Respuesta de la version de un solo jugador.
return [{name:'Deaths', deaths:d.deaths, bossFight:d.bossFight,
playerLoaded:d.playerLoaded, connected:d.connected, self:true}];
}
function renderOverlay(d){
var players = playersOf(d);
var list = document.getElementById('ov-players');
document.getElementById('ov-plate').className = 'plate' + (players.length < 2 ? ' solo' : '');
ensureSlots(list, players.length);
for (var i = 0; i < players.length; i++){
var p = players[i];
var slot = slots[i];
// No character name yet: label based on the mode.
slot.name.textContent = p.name ||
(players.length < 2 ? t('overlay.deaths') : (p.self ? t('overlay.you') : t('overlay.partner')));
slot.num.textContent = p.connected ? p.deaths : '–';
slot.col.className = 'pcol' + (p.connected ? '' : ' off');
slot.note.textContent = '';
if (!p.connected){
slot.note.textContent = p.self ? t('overlay.waiting') : t('overlay.offline');
} else {
slot.note.appendChild(document.createTextNode(t('overlay.deaths')));
if (p.bossFight){
var sep = document.createElement('span');
sep.className = 'sep'; sep.textContent = '·';
var st = document.createElement('span');
st.className = 'st'; st.textContent = '⚔ ' + t('overlay.boss');
slot.note.appendChild(sep); slot.note.appendChild(st);
}
}
var prev = prevByName[p.name];
if (p.connected && prev !== undefined && p.deaths > prev) pulse(slot.num);
prevByName[p.name] = p.connected ? p.deaths : undefined;
}
}
function render(d){
var total = d.deaths;
var display = d.connected ? total : '–';
if (mode === 'overlay'){
renderOverlay(d);
} else {
var cTotal = document.getElementById('c-total');
cTotal.textContent = display;
document.getElementById('c-raw').textContent = d.connected ? d.rawDeaths : '–';
document.getElementById('c-slot').textContent = (d.slot === undefined || d.slot < 0) ? '–' : d.slot;
document.getElementById('c-boss').classList.toggle('on', !!d.bossFight);
document.getElementById('c-build').textContent = d.build ? (t('panel.build') + ': ' + d.build) : '';
if (d.connected && prevTotal !== null && total > prevTotal) pulse(cTotal);
if (!d.connected) setStatus(t('status.waitingGame'), '');
else if (d.playerLoaded === false) setStatus(t('status.menu'), '');
else setStatus(t('status.live'), 'live');
var peers = (d.players || []).filter(function(p){ return !p.self; });
document.getElementById('c-peers-box').hidden = peers.length === 0;
var box = document.getElementById('c-peers');
box.textContent = '';
peers.forEach(function(p){
var row = document.createElement('div');
row.className = 'peer-row' + (p.connected ? '' : ' off');
var n = document.createElement('span'); n.className = 'pn'; n.textContent = p.name;
var s = document.createElement('span'); s.className = 'ps';
s.textContent = p.connected ? (p.playerLoaded ? t('peer.playing') : t('peer.menu')) : t('peer.offline');
var v = document.createElement('span'); v.className = 'pv';
v.textContent = p.connected ? p.deaths : '–';
var left = document.createElement('span');
left.appendChild(n); left.appendChild(document.createTextNode(' ')); left.appendChild(s);
row.appendChild(left); row.appendChild(v);
box.appendChild(row);
});
}
prevTotal = d.connected ? total : null;
}
function poll(){
fetch('/deaths', {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(d){
render(d);
}).catch(function(){
setStatus(t('status.noServer'), 'err');
}).finally(function(){
setTimeout(poll, 1000);
});
}
// ?lang= in the URL overrides the config's language, handy for having
// the overlay in one language and the panel in another without
// touching any files.
var langParam = new URLSearchParams(location.search).get('lang');
fetch('/strings.json' + (langParam ? '?lang=' + encodeURIComponent(langParam) : ''), {cache:'no-store'})
.then(function(r){ return r.json(); })
.then(function(d){ if (d && typeof d === 'object') STR = d; })
.catch(function(){ /* stick with FALLBACK */ })
.finally(function(){ applyStrings(); poll(); });
})();
</script>
</body>
</html>