Scoreboard (HTML)
Moderator: Moderatoren
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
hmm, gerade nochmal getestet, bei mir gehts... muss ich später nochmal schauen
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
Kann im übrigen den
Fully Kiosk Browser
empfehlen, dann siehst immer aus wie ne App....
Mit HTML/PHP kann man aber besser das Design für sich anpassen.
Fully Kiosk Browser
empfehlen, dann siehst immer aus wie ne App....
Mit HTML/PHP kann man aber besser das Design für sich anpassen.
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
Bei mir geht... komisch.
Hier nochmal der Code:
Hier nochmal der Code:
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--app-bg: #e9ecef;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
}
body.dark-mode {
--bg-gray: #2c2c2c;
--app-bg: #121212;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
body { font-family: 'Segoe UI', sans-serif; background-color: var(--app-bg); margin: 0; padding: 20px; display: flex; justify-content: center; transition: 0.3s; color: var(--text-main); }
#app { width: 100%; max-width: 900px; background: var(--card-bg); border-radius: 25px; overflow: hidden; box-shadow: 0 15px 40px rgba(0,0,0,0.1); min-height: 850px; position: relative; }
.nav-tabs { display: flex; background: var(--card-bg); border-bottom: 2px solid var(--border-color); align-items: center; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
.player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid var(--border-color); }
.p-header { display: flex; justify-content: space-between; align-items: center; }
.p-stats { font-size: 14px; color: #777; margin-top: 4px; }
body.dark-mode .p-stats { color: #aaa; }
.del-btn { background: var(--primary-blue); color: white; border: none; padding: 8px 20px; border-radius: 12px; cursor: pointer; font-weight: bold; }
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: var(--text-label); }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background: var(--card-bg); color: var(--text-main); }
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
.free-sb-grid { display: grid; gap: 25px; margin-top: 20px; width: 100%; }
.player-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.player-card {
background: var(--primary-blue);
color: #fff;
padding: 30px 20px;
border-radius: 35px;
text-align: center;
border: 6px solid transparent;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(80,161,228,0.2);
}
.player-card.active-turn { border-color: var(--text-main); border-width: 6px; transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: 80px; font-weight: 800; margin: 5px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; font-weight: 500; }
.controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
.controls-row-bottom { display: flex; justify-content: center; gap: 20px; margin-top: 40px; padding-bottom: 30px; }
.player-controls-mini { display: flex; gap: 15px; justify-content: center; }
.btn-round { width: 70px; height: 70px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--card-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.05); font-size: 30px; color: var(--primary-blue); transition: 0.2s; }
.btn-round-small { width: 55px; height: 55px; border-radius: 50%; border: 1px solid var(--border-color); background: var(--card-bg); font-size: 24px; color: var(--primary-blue); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-round:active, .btn-round-small:active { transform: scale(0.9); }
.btn-swap-small { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-main); }
.primary-btn { width: 100%; padding: 20px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
.restart-btn { background: var(--success); color: white; padding: 20px; border-radius: 15px; border: none; font-size: 20px; font-weight: bold; cursor: pointer; width: 100%; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }
.opt-btn { padding: 15px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-gray); color: var(--text-main); cursor: pointer; font-weight: bold; }
.modal-overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; border-radius: 25px; }
.modal-box { background: var(--card-bg); padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.k-btn { padding: 15px 5px; background: var(--primary-blue); border: none; color: white; border-radius: 12px; font-weight: bold; cursor: pointer; }
#pin-display { font-size: 32px; letter-spacing: 8px; margin-bottom: 20px; height: 40px; color: var(--primary-blue); font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:30px; font-size:11px; background:none; border:none; color:#bbb; cursor:pointer; text-decoration: underline;">Alle Statistiken löschen</button>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option>
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
<option value="Scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label id="race-label">Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group" id="break-mode-container" style="display:none;">
<label>Anstoß-Modus:</label>
<select id="set-break">
<option value="wechsel">Wechselbreak</option>
<option value="winner">Winnerbreak</option>
</select>
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.onPlayerCountChange()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
<div class="settings-grid">
<button class="opt-btn" onclick="app.toggleDarkMode()">🌓 Dark Mode umschalten</button>
<button class="opt-btn" onclick="app.toggleFS()">⛶ Vollbild Modus</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px; font-size:20px; text-transform: uppercase;"></div>
<div id="sb-container"></div>
<div id="winner-area" style="max-width: 400px; margin: 20px auto;"></div>
<div id="controls-area"></div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Kugeln auf dem Tisch?</h3>
<button id="foul-toggle-btn" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-bottom:20px; font-weight:bold; background:none; cursor:pointer;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-top:20px; font-weight:bold; background:none; cursor:pointer; text-transform: uppercase;">Abbrechen</button>
</div>
</div>
<div id="restart-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Partie neustarten?</h3>
<p>Möchtest du den aktuellen Spielstand wirklich zurücksetzen?</p>
<button onclick="app.confirmRestart()" class="restart-btn" style="margin-top:10px;">JA, NEUSTART</button>
<button onclick="app.closeRestartPopup()" style="width:100%; padding:15px; border:2px solid #ccc; color:#666; border-radius:15px; margin-top:10px; font-weight:bold; background:none; cursor:pointer;">ABBRECHEN</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Wer beginnt?</h3>
<p>Bitte Spieler für den Anstoß wählen:</p>
<div id="break-choice-btns"></div>
</div>
</div>
<div id="pin-popup" class="modal-overlay">
<div class="modal-box">
<h3 id="pin-title" style="margin-top:0;">PIN eingeben</h3>
<div id="pin-display"></div>
<div class="kugel-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="k-btn" onclick="app.pressPin(1)">1</button>
<button class="k-btn" onclick="app.pressPin(2)">2</button>
<button class="k-btn" onclick="app.pressPin(3)">3</button>
<button class="k-btn" onclick="app.pressPin(4)">4</button>
<button class="k-btn" onclick="app.pressPin(5)">5</button>
<button class="k-btn" onclick="app.pressPin(6)">6</button>
<button class="k-btn" onclick="app.pressPin(7)">7</button>
<button class="k-btn" onclick="app.pressPin(8)">8</button>
<button class="k-btn" onclick="app.pressPin(9)">9</button>
<button class="k-btn" style="background:#888;" onclick="app.clearPin()">C</button>
<button class="k-btn" onclick="app.pressPin(0)">0</button>
<button class="k-btn" style="background:var(--danger);" onclick="app.closePinPopup()">X</button>
</div>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
correctPin: "12345",
currentPin: "",
pinAction: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
},
toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
},
toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen();
}
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
openPinPopup(action, title) {
this.currentPin = "";
this.pinAction = action;
document.getElementById('pin-display').innerText = "";
document.getElementById('pin-title').innerText = title || "PIN eingeben";
document.getElementById('pin-popup').style.display = 'flex';
},
closePinPopup() { document.getElementById('pin-popup').style.display = 'none'; },
clearPin() { this.currentPin = ""; document.getElementById('pin-display').innerText = ""; },
pressPin(num) {
this.currentPin += num;
document.getElementById('pin-display').innerText = "*".repeat(this.currentPin.length);
if (this.currentPin === this.correctPin) {
const action = this.pinAction;
this.closePinPopup();
action();
} else if (this.currentPin.length >= this.correctPin.length) {
setTimeout(() => { alert("Falscher PIN"); this.clearPin(); }, 100);
}
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
return `<div class="player-db-entry">
<div class="p-header">
<span style="font-weight:bold; font-size:18px;">${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">Löschen</button>
</div>
<div class="p-stats">Bilanz: Siege: ${s.w} | Niederlagen: ${s.l} | HS: ${s.hs || 0}</div>
</div>`;
}).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) {
this.openPinPopup(() => {
this.players.push(n);
this.saveDB();
this.renderPlayerList();
document.getElementById('in-name').value="";
}, "Spieler hinzufügen");
}
},
delPlayer(i) {
this.openPinPopup(() => {
this.players.splice(i,1);
this.saveDB();
this.renderPlayerList();
}, "Spieler löschen");
},
resetStats() {
this.openPinPopup(() => {
this.stats = {};
this.saveDB();
this.renderPlayerList();
}, "Alle Statistiken löschen");
},
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(disc);
// Ausblenden wenn 1 Spieler bei Ball-Games
const breakContainer = document.getElementById('break-mode-container');
if (isBallGame && pCount === 1) {
breakContainer.style.display = 'none';
} else if (isBallGame) {
breakContainer.style.display = 'block';
} else {
breakContainer.style.display = 'none';
}
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
let defVal = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}:</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
}
},
startGame() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
const breakMode = document.getElementById('set-break').value;
this.game = {
disc, names, scores: Array(names.length).fill(0),
activeIdx: 0, balls: 15, race: parseInt(document.getElementById('set-race').value),
breakMode,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })), over: false, recorded: false
};
this.history = []; this.renderGame(); this.nav('game');
},
renderGame() {
const g = this.game;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isFreeSB = g.disc === 'Scoreboard';
const isSinglePlayer = g.names.length === 1;
document.getElementById('game-title').innerText = isFreeSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
const winnerArea = document.getElementById('winner-area');
container.innerHTML = ""; controls.innerHTML = ""; winnerArea.innerHTML = "";
let winnerIdx = !isFreeSB ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
if (isFreeSB) {
container.className = "free-sb-grid";
const cols = g.names.length < 3 ? g.names.length : 3;
container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
} else {
container.className = "sb-layout";
container.style.gridTemplateColumns = "";
}
g.names.forEach((name, i) => {
const stats = g.stats[i];
const isW = i === winnerIdx;
let statsHtml = (isBallGame || isFreeSB) ? "" : `
<div class="stats-row">Aufnahme: ${stats.innings} | GD: ${(g.scores[i] / stats.innings).toFixed(2)}</div>
<div class="stats-row">Serie: ${stats.currentSeries} | HS: ${stats.hs}</div>`;
// Logik für aktiven Rahmen (nur wenn mehr als 1 Spieler)
const showFrame = !isSinglePlayer && ((isBallGame || !isFreeSB) && g.activeIdx === i);
const card = `
<div class="player-wrapper">
<div class="player-card ${showFrame ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}">
<div style="font-weight:bold; font-size:1.2rem;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${statsHtml}
</div>
${(isBallGame || isFreeSB) ? `
<div class="player-controls-mini">
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, -1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>
` : ''}
</div>`;
container.innerHTML += card;
if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
if (isBallGame || isFreeSB) {
controls.innerHTML = `
<div class="controls-row-bottom">
<button class="btn-round" onclick="app.openRestartPopup()">↺</button>
<button class="btn-round" onclick="app.undo()">↶</button>
</div>`;
} else {
controls.innerHTML = `
<div class="controls-row">
<button class="btn-round" onclick="app.openRestartPopup()">↺</button>
<button class="btn-round" onclick="app.undo()">↶</button>
<button class="btn-round" onclick="app.changeScore141(-1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round" onclick="app.changeScore141(1)" ${g.over?'disabled':''}>+</button>
<button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button>
<button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button>
<button class="btn-round" style="color:#f1c40f;" onclick="app.triggerFoulLogic()" ${g.over?'disabled':''}>⚡</button>
</div>`;
}
if(g.over) {
winnerArea.innerHTML = `<button class="restart-btn" onclick="app.handleRematch()">REMATCH STARTEN</button>`;
}
},
// Zweite Anpassung: Rematch Logik
handleRematch() {
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && this.game.names.length > 1) {
this.openBreakChoicePopup();
} else {
this.startGame();
}
},
openBreakChoicePopup() {
const popup = document.getElementById('break-choice-popup');
const btnArea = document.getElementById('break-choice-btns');
btnArea.innerHTML = "";
this.game.names.forEach((name, idx) => {
const btn = document.createElement('button');
btn.className = 'primary-btn';
btn.style.marginTop = '10px';
btn.innerText = name;
btn.onclick = () => {
popup.style.display = 'none';
this.startGame(); // Initialisiert Spiel
this.game.activeIdx = idx; // Setzt gewählten Spieler aktiv
this.renderGame();
};
btnArea.appendChild(btn);
});
popup.style.display = 'flex';
},
changeScoreDir(idx, v) {
this.saveStep();
const g = this.game;
g.scores[idx] = Math.max(0, g.scores[idx] + v);
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
if (isBallGame && v === 1 && g.names.length > 1) {
if (g.breakMode === 'wechsel') {
g.activeIdx = (g.activeIdx === 0) ? 1 : 0;
} else if (g.breakMode === 'winner') {
g.activeIdx = idx;
}
}
this.renderGame();
},
changeScore141(v) {
const p = this.game.activeIdx;
if(v < 0) {
if (this.game.balls >= 15) return;
this.saveStep();
this.game.scores[p] += v;
this.game.balls++;
this.game.stats[p].currentSeries = Math.max(0, this.game.stats[p].currentSeries - 1);
} else {
this.saveStep();
this.game.scores[p] += v;
this.game.stats[p].currentSeries++;
if (this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.renderGame();
},
rack141() {
this.saveStep();
const p = this.game.activeIdx;
const pts = (this.game.balls - 1);
this.game.scores[p] += pts;
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15;
this.renderGame();
},
switchPlayer() {
this.saveStep();
this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++;
this.renderGame();
},
triggerFoulLogic() {
this.saveStep();
const pIdx = this.game.activeIdx;
const stats = this.game.stats[pIdx];
if (pIdx === 0 && stats.innings === 1 && stats.currentSeries === 0) {
this.game.scores[pIdx] -= 2;
} else {
this.game.scores[pIdx] -= 1;
this.game.stats[pIdx].currentSeries = 0;
if (this.game.names.length > 1) {
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++;
} else {
this.game.stats[pIdx].innings++;
}
}
this.renderGame();
},
openKugelPopup() {
this.foulActive = false;
const foulBtn = document.getElementById('foul-toggle-btn');
if (foulBtn) {
foulBtn.style.color = "#50a1e4";
foulBtn.style.borderColor = "#50a1e4";
}
const grid = document.getElementById('kugel-grid');
grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'k-btn'; b.innerText = i;
if (i > this.game.balls) { b.disabled = true; b.style.opacity = "0.3"; }
else {
b.onclick = () => {
this.saveStep();
let pts = (this.game.balls - i);
const p = this.game.activeIdx;
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
} else { this.game.stats[p].currentSeries = 0; }
this.game.balls = i <= 1 ? 15 : i;
this.closeKugelPopup();
this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
closeKugelPopup() {
document.getElementById('kugel-popup').style.display = 'none';
this.foulActive = false;
},
toggleFoulFrame() {
this.foulActive = !this.foulActive;
const btn = document.getElementById('foul-toggle-btn');
btn.style.color = this.foulActive ? "red" : "#50a1e4";
btn.style.borderColor = this.foulActive ? "red" : "#50a1e4";
},
openRestartPopup() { document.getElementById('restart-popup').style.display = 'flex'; },
closeRestartPopup() { document.getElementById('restart-popup').style.display = 'none'; },
confirmRestart() { this.closeRestartPopup(); this.startGame(); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
if(wIdx === -1) return;
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if (this.game.stats[i].hs > (this.stats[n].hs || 0)) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
}
};
app.init();
</script>
</body>
</html>- Dateianhänge
-
- Scoreboard_01.png (45.52 KiB) 968 mal betrachtet
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
und hier nochmal ohne "Container"....
Mit dem Fully Kiosk Browser App-Feeling :)
Wake-Lock aktiv, Tablet sollte nicht in den Ruhemodus gehen.
Mit dem Fully Kiosk Browser App-Feeling :)
Wake-Lock aktiv, Tablet sollte nicht in den Ruhemodus gehen.
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--app-bg: #e9ecef;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
}
body.dark-mode {
--bg-gray: #2c2c2c;
--app-bg: #121212;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
/* ANPASSUNG: Body nutzt jetzt die volle Breite ohne Zentrierung */
body {
font-family: 'Segoe UI', sans-serif;
background-color: var(--card-bg); /* Hintergrundfarbe an App angepasst */
margin: 0;
padding: 0;
display: block;
transition: 0.3s;
color: var(--text-main);
}
/* ANPASSUNG: #app füllt nun den ganzen Bildschirm aus, kein fester Rahmen mehr */
#app {
width: 100%;
max-width: 100%;
background: var(--card-bg);
border-radius: 0;
overflow: hidden;
box-shadow: none;
min-height: 100vh;
position: relative;
}
.nav-tabs { display: flex; background: var(--card-bg); border-bottom: 2px solid var(--border-color); align-items: center; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
.player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid var(--border-color); }
.p-header { display: flex; justify-content: space-between; align-items: center; }
.p-stats { font-size: 14px; color: #777; margin-top: 4px; }
body.dark-mode .p-stats { color: #aaa; }
.del-btn { background: var(--primary-blue); color: white; border: none; padding: 8px 20px; border-radius: 12px; cursor: pointer; font-weight: bold; }
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: var(--text-label); }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background: var(--card-bg); color: var(--text-main); }
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
.free-sb-grid { display: grid; gap: 25px; margin-top: 20px; width: 100%; }
.player-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.player-card {
background: var(--primary-blue);
color: #fff;
padding: 30px 20px;
border-radius: 35px;
text-align: center;
border: 6px solid transparent;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(80,161,228,0.2);
}
.player-card.active-turn { border-color: var(--text-main); border-width: 6px; transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: 80px; font-weight: 800; margin: 5px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; font-weight: 500; }
.controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
.controls-row-bottom { display: flex; justify-content: center; gap: 20px; margin-top: 40px; padding-bottom: 30px; }
.player-controls-mini { display: flex; gap: 15px; justify-content: center; }
.btn-round { width: 70px; height: 70px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--card-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.05); font-size: 30px; color: var(--primary-blue); transition: 0.2s; }
.btn-round-small { width: 55px; height: 55px; border-radius: 50%; border: 1px solid var(--border-color); background: var(--card-bg); font-size: 24px; color: var(--primary-blue); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-round:active, .btn-round-small:active { transform: scale(0.9); }
.btn-swap-small { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-main); }
.primary-btn { width: 100%; padding: 20px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
.restart-btn { background: var(--success); color: white; padding: 20px; border-radius: 15px; border: none; font-size: 20px; font-weight: bold; cursor: pointer; width: 100%; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }
.opt-btn { padding: 15px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-gray); color: var(--text-main); cursor: pointer; font-weight: bold; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; }
.modal-box { background: var(--card-bg); padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.k-btn { padding: 15px 5px; background: var(--primary-blue); border: none; color: white; border-radius: 12px; font-weight: bold; cursor: pointer; }
#pin-display { font-size: 32px; letter-spacing: 8px; margin-bottom: 20px; height: 40px; color: var(--primary-blue); font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:30px; font-size:11px; background:none; border:none; color:#bbb; cursor:pointer; text-decoration: underline;">Alle Statistiken löschen</button>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option>
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
<option value="Scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label id="race-label">Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group" id="break-mode-container" style="display:none;">
<label>Anstoß-Modus:</label>
<select id="set-break">
<option value="wechsel">Wechselbreak</option>
<option value="winner">Winnerbreak</option>
</select>
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.onPlayerCountChange()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
<div class="settings-grid">
<button class="opt-btn" onclick="app.toggleDarkMode()">🌓 Dark Mode umschalten</button>
<button class="opt-btn" onclick="app.toggleFS()">⛶ Vollbild Modus</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px; font-size:20px; text-transform: uppercase;"></div>
<div id="sb-container"></div>
<div id="winner-area" style="max-width: 400px; margin: 20px auto;"></div>
<div id="controls-area"></div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Kugeln auf dem Tisch?</h3>
<button id="foul-toggle-btn" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-bottom:20px; font-weight:bold; background:none; cursor:pointer;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-top:20px; font-weight:bold; background:none; cursor:pointer; text-transform: uppercase;">Abbrechen</button>
</div>
</div>
<div id="restart-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Partie neustarten?</h3>
<p>Möchtest du den aktuellen Spielstand wirklich zurücksetzen?</p>
<button onclick="app.confirmRestart()" class="restart-btn" style="margin-top:10px;">JA, NEUSTART</button>
<button onclick="app.closeRestartPopup()" style="width:100%; padding:15px; border:2px solid #ccc; color:#666; border-radius:15px; margin-top:10px; font-weight:bold; background:none; cursor:pointer;">ABBRECHEN</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Wer beginnt?</h3>
<p>Bitte Spieler für den Anstoß wählen:</p>
<div id="break-choice-btns"></div>
</div>
</div>
<div id="pin-popup" class="modal-overlay">
<div class="modal-box">
<h3 id="pin-title" style="margin-top:0;">PIN eingeben</h3>
<div id="pin-display"></div>
<div class="kugel-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="k-btn" onclick="app.pressPin(1)">1</button>
<button class="k-btn" onclick="app.pressPin(2)">2</button>
<button class="k-btn" onclick="app.pressPin(3)">3</button>
<button class="k-btn" onclick="app.pressPin(4)">4</button>
<button class="k-btn" onclick="app.pressPin(5)">5</button>
<button class="k-btn" onclick="app.pressPin(6)">6</button>
<button class="k-btn" onclick="app.pressPin(7)">7</button>
<button class="k-btn" onclick="app.pressPin(8)">8</button>
<button class="k-btn" onclick="app.pressPin(9)">9</button>
<button class="k-btn" style="background:#888;" onclick="app.clearPin()">C</button>
<button class="k-btn" onclick="app.pressPin(0)">0</button>
<button class="k-btn" style="background:var(--danger);" onclick="app.closePinPopup()">X</button>
</div>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
correctPin: "12345", currentPin: "", pinAction: null,
wakeLock: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
// WAKE LOCK INTEGRATION
this.requestWakeLock();
document.addEventListener('visibilitychange', () => {
if (this.wakeLock !== null && document.visibilityState === 'visible') this.requestWakeLock();
});
},
async requestWakeLock() {
try {
if ('wakeLock' in navigator) {
this.wakeLock = await navigator.wakeLock.request('screen');
}
} catch (err) { console.log("WakeLock error"); }
},
// AB HIER DEINE ORIGINAL LOGIK (UNVERÄNDERT)
toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
},
toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen();
}
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
openPinPopup(action, title) {
this.currentPin = "";
this.pinAction = action;
document.getElementById('pin-display').innerText = "";
document.getElementById('pin-title').innerText = title || "PIN eingeben";
document.getElementById('pin-popup').style.display = 'flex';
},
closePinPopup() { document.getElementById('pin-popup').style.display = 'none'; },
clearPin() { this.currentPin = ""; document.getElementById('pin-display').innerText = ""; },
pressPin(num) {
this.currentPin += num;
document.getElementById('pin-display').innerText = "*".repeat(this.currentPin.length);
if (this.currentPin === this.correctPin) {
const action = this.pinAction;
this.closePinPopup();
action();
} else if (this.currentPin.length >= this.correctPin.length) {
setTimeout(() => { alert("Falscher PIN"); this.clearPin(); }, 100);
}
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
return `<div class="player-db-entry">
<div class="p-header">
<span style="font-weight:bold; font-size:18px;">${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">Löschen</button>
</div>
<div class="p-stats">Bilanz: Siege: ${s.w} | Niederlagen: ${s.l} | HS: ${s.hs || 0}</div>
</div>`;
}).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) {
this.openPinPopup(() => {
this.players.push(n);
this.saveDB();
this.renderPlayerList();
document.getElementById('in-name').value="";
}, "Spieler hinzufügen");
}
},
delPlayer(i) {
this.openPinPopup(() => {
this.players.splice(i,1);
this.saveDB();
this.renderPlayerList();
}, "Spieler löschen");
},
resetStats() {
this.openPinPopup(() => {
this.stats = {};
this.saveDB();
this.renderPlayerList();
}, "Alle Statistiken löschen");
},
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(disc);
const breakContainer = document.getElementById('break-mode-container');
if (isBallGame && pCount === 1) breakContainer.style.display = 'none';
else if (isBallGame) breakContainer.style.display = 'block';
else breakContainer.style.display = 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
let defVal = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}:</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
}
},
startGame() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
const breakMode = document.getElementById('set-break').value;
this.game = {
disc, names, scores: Array(names.length).fill(0),
activeIdx: 0, balls: 15, race: parseInt(document.getElementById('set-race').value),
breakMode, stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })), over: false, recorded: false
};
this.history = []; this.renderGame(); this.nav('game');
},
renderGame() {
const g = this.game;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isFreeSB = g.disc === 'Scoreboard';
const isSinglePlayer = g.names.length === 1;
document.getElementById('game-title').innerText = isFreeSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
const winnerArea = document.getElementById('winner-area');
container.innerHTML = ""; controls.innerHTML = ""; winnerArea.innerHTML = "";
let winnerIdx = !isFreeSB ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
if (isFreeSB) {
container.className = "free-sb-grid";
const cols = g.names.length < 3 ? g.names.length : 3;
container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
} else {
container.className = "sb-layout";
}
g.names.forEach((name, i) => {
const stats = g.stats[i];
const isW = i === winnerIdx;
let statsHtml = (isBallGame || isFreeSB) ? "" : `
<div class="stats-row">Aufnahme: ${stats.innings} | GD: ${(g.scores[i] / stats.innings).toFixed(2)}</div>
<div class="stats-row">Serie: ${stats.currentSeries} | HS: ${stats.hs}</div>`;
const showFrame = !isSinglePlayer && ((isBallGame || !isFreeSB) && g.activeIdx === i);
const card = `
<div class="player-wrapper">
<div class="player-card ${showFrame ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}">
<div style="font-weight:bold; font-size:1.2rem;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${statsHtml}
</div>
${(isBallGame || isFreeSB) ? `
<div class="player-controls-mini">
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, -1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>
` : ''}
</div>`;
container.innerHTML += card;
if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
if (isBallGame || isFreeSB) {
controls.innerHTML = `<div class="controls-row-bottom"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button></div>`;
} else {
controls.innerHTML = `<div class="controls-row"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button><button class="btn-round" onclick="app.changeScore141(-1)" ${g.over?'disabled':''}>–</button><button class="btn-round" onclick="app.changeScore141(1)" ${g.over?'disabled':''}>+</button><button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button><button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button><button class="btn-round" style="color:#f1c40f;" onclick="app.triggerFoulLogic()" ${g.over?'disabled':''}>⚡</button></div>`;
}
if(g.over) winnerArea.innerHTML = `<button class="restart-btn" onclick="app.handleRematch()">REMATCH STARTEN</button>`;
},
handleRematch() {
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && this.game.names.length > 1) this.openBreakChoicePopup();
else this.startGame();
},
openBreakChoicePopup() {
const popup = document.getElementById('break-choice-popup');
const btnArea = document.getElementById('break-choice-btns');
btnArea.innerHTML = "";
this.game.names.forEach((name, idx) => {
const btn = document.createElement('button');
btn.className = 'primary-btn';
btn.style.marginTop = '10px';
btn.innerText = name;
btn.onclick = () => { popup.style.display = 'none'; this.startGame(); this.game.activeIdx = idx; this.renderGame(); };
btnArea.appendChild(btn);
});
popup.style.display = 'flex';
},
changeScoreDir(idx, v) {
this.saveStep();
this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && v === 1 && this.game.names.length > 1) {
if (this.game.breakMode === 'wechsel') this.game.activeIdx = (this.game.activeIdx === 0) ? 1 : 0;
else if (this.game.breakMode === 'winner') this.game.activeIdx = idx;
}
this.renderGame();
},
changeScore141(v) {
const p = this.game.activeIdx;
if(v < 0) {
if (this.game.balls >= 15) return;
this.saveStep();
this.game.scores[p] += v;
this.game.balls++;
this.game.stats[p].currentSeries = Math.max(0, this.game.stats[p].currentSeries - 1);
} else {
this.saveStep();
this.game.scores[p] += v;
this.game.stats[p].currentSeries++;
if (this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.renderGame();
},
rack141() {
this.saveStep();
const p = this.game.activeIdx;
const pts = (this.game.balls - 1);
this.game.scores[p] += pts;
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15;
this.renderGame();
},
switchPlayer() {
this.saveStep();
this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++;
this.renderGame();
},
triggerFoulLogic() {
this.saveStep();
const pIdx = this.game.activeIdx;
const stats = this.game.stats[pIdx];
if (pIdx === 0 && stats.innings === 1 && stats.currentSeries === 0) this.game.scores[pIdx] -= 2;
else {
this.game.scores[pIdx] -= 1;
this.game.stats[pIdx].currentSeries = 0;
if (this.game.names.length > 1) { this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length; this.game.stats[this.game.activeIdx].innings++; }
else stats.innings++;
}
this.renderGame();
},
openKugelPopup() {
this.foulActive = false;
const foulBtn = document.getElementById('foul-toggle-btn');
if (foulBtn) { foulBtn.style.color = "#50a1e4"; foulBtn.style.borderColor = "#50a1e4"; }
const grid = document.getElementById('kugel-grid');
grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'k-btn'; b.innerText = i;
if (i > this.game.balls) { b.disabled = true; b.style.opacity = "0.3"; }
else {
b.onclick = () => {
this.saveStep();
let pts = (this.game.balls - i);
const p = this.game.activeIdx;
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
} else this.game.stats[p].currentSeries = 0;
this.game.balls = i <= 1 ? 15 : i;
this.closeKugelPopup();
this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; this.foulActive = false; },
toggleFoulFrame() { this.foulActive = !this.foulActive; const btn = document.getElementById('foul-toggle-btn'); btn.style.color = this.foulActive ? "red" : "#50a1e4"; btn.style.borderColor = this.foulActive ? "red" : "#50a1e4"; },
openRestartPopup() { document.getElementById('restart-popup').style.display = 'flex'; },
closeRestartPopup() { document.getElementById('restart-popup').style.display = 'none'; },
confirmRestart() { this.closeRestartPopup(); this.startGame(); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
if(wIdx === -1) return;
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if (this.game.stats[i].hs > (this.stats[n].hs || 0)) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
}
};
app.init();
</script>
</body>
</html>- AIMhAK
- Pomeranzenkiller

- Beiträge: 63
- Registriert: 24.10.12 19:34
- Reputation: 24
- Name: Aaron
- Playing cue: Mezz EC7-C + ExPro
- Tip: G2 Medium
- Break Cue: Mezz Power Break Kai PBKW-T
- Jump Cue: Predator Air 2 Jump
- Wohnort: Alsheim
Re: Scoreboard (HTML)
Habe es am PC mit Edge, Chrome und Firefox getestet und am Smartphone mit Chrome und Samsung Webbrowser...
Das Abfragefenster erscheint leider nicht.
Das Abfragefenster erscheint leider nicht.
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
schon komisch... bei mir gehts
egal, teste mal diesen
egal, teste mal diesen
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--app-bg: #e9ecef;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
}
body.dark-mode {
--bg-gray: #2c2c2c;
--app-bg: #121212;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
/* ANPASSUNG: Body nutzt jetzt die volle Breite ohne Zentrierung */
body {
font-family: 'Segoe UI', sans-serif;
background-color: var(--card-bg); /* Hintergrundfarbe an App angepasst */
margin: 0;
padding: 0;
display: block;
transition: 0.3s;
color: var(--text-main);
}
/* ANPASSUNG: #app füllt nun den ganzen Bildschirm aus, kein fester Rahmen mehr */
#app {
width: 100%;
max-width: 100%;
background: var(--card-bg);
border-radius: 0;
overflow: hidden;
box-shadow: none;
min-height: 100vh;
position: relative;
}
.nav-tabs { display: flex; background: var(--card-bg); border-bottom: 2px solid var(--border-color); align-items: center; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
.player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid var(--border-color); }
.p-header { display: flex; justify-content: space-between; align-items: center; }
.p-stats { font-size: 14px; color: #777; margin-top: 4px; }
body.dark-mode .p-stats { color: #aaa; }
.del-btn { background: var(--primary-blue); color: white; border: none; padding: 8px 20px; border-radius: 12px; cursor: pointer; font-weight: bold; }
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: var(--text-label); }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background: var(--card-bg); color: var(--text-main); }
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
.free-sb-grid { display: grid; gap: 25px; margin-top: 20px; width: 100%; }
.player-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.player-card {
background: var(--primary-blue);
color: #fff;
padding: 30px 20px;
border-radius: 35px;
text-align: center;
border: 6px solid transparent;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(80,161,228,0.2);
}
.player-card.active-turn { border-color: var(--text-main); border-width: 6px; transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: 80px; font-weight: 800; margin: 5px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; font-weight: 500; }
.controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
.controls-row-bottom { display: flex; justify-content: center; gap: 20px; margin-top: 40px; padding-bottom: 30px; }
.player-controls-mini { display: flex; gap: 15px; justify-content: center; }
.btn-round { width: 70px; height: 70px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--card-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.05); font-size: 30px; color: var(--primary-blue); transition: 0.2s; }
.btn-round-small { width: 55px; height: 55px; border-radius: 50%; border: 1px solid var(--border-color); background: var(--card-bg); font-size: 24px; color: var(--primary-blue); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-round:active, .btn-round-small:active { transform: scale(0.9); }
.btn-swap-small { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-main); }
.primary-btn { width: 100%; padding: 20px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
.restart-btn { background: var(--success); color: white; padding: 20px; border-radius: 15px; border: none; font-size: 20px; font-weight: bold; cursor: pointer; width: 100%; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }
.opt-btn { padding: 15px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-gray); color: var(--text-main); cursor: pointer; font-weight: bold; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; }
.modal-box { background: var(--card-bg); padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.k-btn { padding: 15px 5px; background: var(--primary-blue); border: none; color: white; border-radius: 12px; font-weight: bold; cursor: pointer; }
#pin-display { font-size: 32px; letter-spacing: 8px; margin-bottom: 20px; height: 40px; color: var(--primary-blue); font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:30px; font-size:11px; background:none; border:none; color:#bbb; cursor:pointer; text-decoration: underline;">Alle Statistiken löschen</button>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option>
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
<option value="Scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label id="race-label">Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group" id="break-mode-container" style="display:none;">
<label>Anstoß-Modus:</label>
<select id="set-break">
<option value="wechsel">Wechselbreak</option>
<option value="winner">Winnerbreak</option>
</select>
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.onPlayerCountChange()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
<div class="settings-grid">
<button class="opt-btn" onclick="app.toggleDarkMode()">🌓 Dark Mode umschalten</button>
<button class="opt-btn" onclick="app.toggleFS()">⛶ Vollbild Modus</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px; font-size:20px; text-transform: uppercase;"></div>
<div id="sb-container"></div>
<div id="winner-area" style="max-width: 400px; margin: 20px auto;"></div>
<div id="controls-area"></div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Kugeln auf dem Tisch?</h3>
<button id="foul-toggle-btn" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-bottom:20px; font-weight:bold; background:none; cursor:pointer;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-top:20px; font-weight:bold; background:none; cursor:pointer; text-transform: uppercase;">Abbrechen</button>
</div>
</div>
<div id="restart-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Partie neustarten?</h3>
<p>Möchtest du den aktuellen Spielstand wirklich zurücksetzen?</p>
<button onclick="app.confirmRestart()" class="restart-btn" style="margin-top:10px;">JA, NEUSTART</button>
<button onclick="app.closeRestartPopup()" style="width:100%; padding:15px; border:2px solid #ccc; color:#666; border-radius:15px; margin-top:10px; font-weight:bold; background:none; cursor:pointer;">ABBRECHEN</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Wer beginnt?</h3>
<p>Bitte Spieler für den Anstoß wählen:</p>
<div id="break-choice-btns"></div>
</div>
</div>
<div id="pin-popup" class="modal-overlay">
<div class="modal-box">
<h3 id="pin-title" style="margin-top:0;">PIN eingeben</h3>
<div id="pin-display"></div>
<div class="kugel-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="k-btn" onclick="app.pressPin(1)">1</button>
<button class="k-btn" onclick="app.pressPin(2)">2</button>
<button class="k-btn" onclick="app.pressPin(3)">3</button>
<button class="k-btn" onclick="app.pressPin(4)">4</button>
<button class="k-btn" onclick="app.pressPin(5)">5</button>
<button class="k-btn" onclick="app.pressPin(6)">6</button>
<button class="k-btn" onclick="app.pressPin(7)">7</button>
<button class="k-btn" onclick="app.pressPin(8)">8</button>
<button class="k-btn" onclick="app.pressPin(9)">9</button>
<button class="k-btn" style="background:#888;" onclick="app.clearPin()">C</button>
<button class="k-btn" onclick="app.pressPin(0)">0</button>
<button class="k-btn" style="background:var(--danger);" onclick="app.closePinPopup()">X</button>
</div>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
correctPin: "12345", currentPin: "", pinAction: null,
wakeLock: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
// WAKE LOCK INTEGRATION
this.requestWakeLock();
document.addEventListener('visibilitychange', () => {
if (this.wakeLock !== null && document.visibilityState === 'visible') this.requestWakeLock();
});
},
async requestWakeLock() {
try {
if ('wakeLock' in navigator) {
this.wakeLock = await navigator.wakeLock.request('screen');
}
} catch (err) { console.log("WakeLock error"); }
},
toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
},
toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen();
}
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
openPinPopup(action, title) {
this.currentPin = "";
this.pinAction = action;
document.getElementById('pin-display').innerText = "";
document.getElementById('pin-title').innerText = title || "PIN eingeben";
document.getElementById('pin-popup').style.display = 'flex';
},
closePinPopup() { document.getElementById('pin-popup').style.display = 'none'; },
clearPin() { this.currentPin = ""; document.getElementById('pin-display').innerText = ""; },
pressPin(num) {
this.currentPin += num;
document.getElementById('pin-display').innerText = "*".repeat(this.currentPin.length);
if (this.currentPin === this.correctPin) {
const action = this.pinAction;
this.closePinPopup();
action();
} else if (this.currentPin.length >= this.correctPin.length) {
setTimeout(() => { alert("Falscher PIN"); this.clearPin(); }, 100);
}
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
return `<div class="player-db-entry">
<div class="p-header">
<span style="font-weight:bold; font-size:18px;">${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">Löschen</button>
</div>
<div class="p-stats">Bilanz: Siege: ${s.w} | Niederlagen: ${s.l} | HS: ${s.hs || 0}</div>
</div>`;
}).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) {
this.openPinPopup(() => {
this.players.push(n);
this.saveDB();
this.renderPlayerList();
document.getElementById('in-name').value="";
}, "Spieler hinzufügen");
}
},
delPlayer(i) {
this.openPinPopup(() => {
this.players.splice(i,1);
this.saveDB();
this.renderPlayerList();
}, "Spieler löschen");
},
resetStats() {
this.openPinPopup(() => {
this.stats = {};
this.saveDB();
this.renderPlayerList();
}, "Alle Statistiken löschen");
},
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(disc);
const breakContainer = document.getElementById('break-mode-container');
if (isBallGame && pCount === 1) breakContainer.style.display = 'none';
else if (isBallGame) breakContainer.style.display = 'block';
else breakContainer.style.display = 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
let defVal = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}:</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
}
},
startGame() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
// FIX: Spielerzahl prüfen bei 8/9/10-Ball
const ballGames = ["8-Ball", "9-Ball", "10-Ball"];
if (ballGames.includes(disc) && names.length < 2) {
alert("Für diese Disziplin werden mindestens 2 Spieler benötigt!");
return;
}
const breakMode = document.getElementById('set-break').value;
this.game = {
disc, names,
scores: Array(names.length).fill(0),
activeIdx: 0,
balls: 15,
race: parseInt(document.getElementById('set-race').value),
breakMode,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })),
over: false,
recorded: false
};
this.history = [];
this.renderGame();
this.nav('game');
},
renderGame() {
const g = this.game;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isFreeSB = g.disc === 'Scoreboard';
const isSinglePlayer = g.names.length === 1;
document.getElementById('game-title').innerText = isFreeSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
const winnerArea = document.getElementById('winner-area');
container.innerHTML = ""; controls.innerHTML = ""; winnerArea.innerHTML = "";
let winnerIdx = !isFreeSB ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
if (isFreeSB) {
container.className = "free-sb-grid";
const cols = g.names.length < 3 ? g.names.length : 3;
container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
} else {
container.className = "sb-layout";
}
g.names.forEach((name, i) => {
const stats = g.stats[i];
const isW = i === winnerIdx;
let statsHtml = (isBallGame || isFreeSB) ? "" : `
<div class="stats-row">Aufnahme: ${stats.innings} | GD: ${(g.scores[i] / stats.innings).toFixed(2)}</div>
<div class="stats-row">Serie: ${stats.currentSeries} | HS: ${stats.hs}</div>`;
const showFrame = !isSinglePlayer && ((isBallGame || !isFreeSB) && g.activeIdx === i);
const card = `
<div class="player-wrapper">
<div class="player-card ${showFrame ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}">
<div style="font-weight:bold; font-size:1.2rem;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${statsHtml}
</div>
${(isBallGame || isFreeSB) ? `
<div class="player-controls-mini">
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, -1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>
` : ''}
</div>`;
container.innerHTML += card;
if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
if (isBallGame || isFreeSB) {
controls.innerHTML = `<div class="controls-row-bottom"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button></div>`;
} else {
controls.innerHTML = `<div class="controls-row"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button><button class="btn-round" onclick="app.changeScore141(-1)" ${g.over?'disabled':''}>–</button><button class="btn-round" onclick="app.changeScore141(1)" ${g.over?'disabled':''}>+</button><button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button><button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button><button class="btn-round" style="color:#f1c40f;" onclick="app.triggerFoulLogic()" ${g.over?'disabled':''}>⚡</button></div>`;
}
if(g.over) winnerArea.innerHTML = `<button class="restart-btn" onclick="app.handleRematch()">REMATCH STARTEN</button>`;
},
handleRematch() {
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && this.game.names && this.game.names.length >= 2) this.openBreakChoicePopup();
else this.startGame();
},
openBreakChoicePopup() {
const popup = document.getElementById('break-choice-popup');
const btnArea = document.getElementById('break-choice-btns');
btnArea.innerHTML = "";
this.game.names.forEach((name, idx) => {
const btn = document.createElement('button');
btn.className = 'primary-btn';
btn.style.marginTop = '10px';
btn.innerText = name;
btn.onclick = () => { popup.style.display = 'none'; this.startGame(); this.game.activeIdx = idx; this.renderGame(); };
btnArea.appendChild(btn);
});
popup.style.display = 'flex';
},
changeScoreDir(idx, v) {
this.saveStep();
this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && v === 1 && this.game.names.length > 1) {
if (this.game.breakMode === 'wechsel') this.game.activeIdx = (this.game.activeIdx === 0) ? 1 : 0;
else if (this.game.breakMode === 'winner') this.game.activeIdx = idx;
}
this.renderGame();
},
changeScore141(v) {
const p = this.game.activeIdx;
if(v < 0) {
if (this.game.balls >= 15) return;
this.saveStep();
this.game.scores[p] += v;
this.game.balls++;
this.game.stats[p].currentSeries = Math.max(0, this.game.stats[p].currentSeries - 1);
} else {
this.saveStep();
this.game.scores[p] += v;
this.game.stats[p].currentSeries++;
if (this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.renderGame();
},
rack141() {
this.saveStep();
const p = this.game.activeIdx;
const pts = (this.game.balls - 1);
this.game.scores[p] += pts;
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15;
this.renderGame();
},
switchPlayer() {
this.saveStep();
this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++;
this.renderGame();
},
triggerFoulLogic() {
this.saveStep();
const pIdx = this.game.activeIdx;
const stats = this.game.stats[pIdx];
if (pIdx === 0 && stats.innings === 1 && stats.currentSeries === 0) this.game.scores[pIdx] -= 2;
else {
this.game.scores[pIdx] -= 1;
this.game.stats[pIdx].currentSeries = 0;
if (this.game.names.length > 1) { this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length; this.game.stats[this.game.activeIdx].innings++; }
else stats.innings++;
}
this.renderGame();
},
openKugelPopup() {
this.foulActive = false;
const foulBtn = document.getElementById('foul-toggle-btn');
if (foulBtn) { foulBtn.style.color = "#50a1e4"; foulBtn.style.borderColor = "#50a1e4"; }
const grid = document.getElementById('kugel-grid');
grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'k-btn'; b.innerText = i;
if (i > this.game.balls) { b.disabled = true; b.style.opacity = "0.3"; }
else {
b.onclick = () => {
this.saveStep();
let pts = (this.game.balls - i);
const p = this.game.activeIdx;
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
} else this.game.stats[p].currentSeries = 0;
this.game.balls = i <= 1 ? 15 : i;
this.closeKugelPopup();
this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; this.foulActive = false; },
toggleFoulFrame() { this.foulActive = !this.foulActive; const btn = document.getElementById('foul-toggle-btn'); btn.style.color = this.foulActive ? "red" : "#50a1e4"; btn.style.borderColor = this.foulActive ? "red" : "#50a1e4"; },
openRestartPopup() { document.getElementById('restart-popup').style.display = 'flex'; },
closeRestartPopup() { document.getElementById('restart-popup').style.display = 'none'; },
confirmRestart() { this.closeRestartPopup(); this.startGame(); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
if(wIdx === -1) return;
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if (this.game.stats[i].hs > (this.stats[n].hs || 0)) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
}
};
app.init();
</script>
</body>
</html>-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
oder diesen
einfach beide ausprobieren
einfach beide ausprobieren
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--app-bg: #e9ecef;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
}
body.dark-mode {
--bg-gray: #2c2c2c;
--app-bg: #121212;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
/* ANPASSUNG: Body nutzt jetzt die volle Breite ohne Zentrierung */
body {
font-family: 'Segoe UI', sans-serif;
background-color: var(--card-bg); /* Hintergrundfarbe an App angepasst */
margin: 0;
padding: 0;
display: block;
transition: 0.3s;
color: var(--text-main);
}
/* ANPASSUNG: #app füllt nun den ganzen Bildschirm aus, kein fester Rahmen mehr */
#app {
width: 100%;
max-width: 100%;
background: var(--card-bg);
border-radius: 0;
overflow: hidden;
box-shadow: none;
min-height: 100vh;
position: relative;
}
.nav-tabs { display: flex; background: var(--card-bg); border-bottom: 2px solid var(--border-color); align-items: center; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
.player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid var(--border-color); }
.p-header { display: flex; justify-content: space-between; align-items: center; }
.p-stats { font-size: 14px; color: #777; margin-top: 4px; }
body.dark-mode .p-stats { color: #aaa; }
.del-btn { background: var(--primary-blue); color: white; border: none; padding: 8px 20px; border-radius: 12px; cursor: pointer; font-weight: bold; }
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: var(--text-label); }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background: var(--card-bg); color: var(--text-main); }
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
.free-sb-grid { display: grid; gap: 25px; margin-top: 20px; width: 100%; }
.player-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.player-card {
background: var(--primary-blue);
color: #fff;
padding: 30px 20px;
border-radius: 35px;
text-align: center;
border: 6px solid transparent;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(80,161,228,0.2);
}
.player-card.active-turn { border-color: var(--text-main); border-width: 6px; transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: 80px; font-weight: 800; margin: 5px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; font-weight: 500; }
.controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
.controls-row-bottom { display: flex; justify-content: center; gap: 20px; margin-top: 40px; padding-bottom: 30px; }
.player-controls-mini { display: flex; gap: 15px; justify-content: center; }
.btn-round { width: 70px; height: 70px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--card-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.05); font-size: 30px; color: var(--primary-blue); transition: 0.2s; }
.btn-round-small { width: 55px; height: 55px; border-radius: 50%; border: 1px solid var(--border-color); background: var(--card-bg); font-size: 24px; color: var(--primary-blue); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-round:active, .btn-round-small:active { transform: scale(0.9); }
.btn-swap-small { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-main); }
.primary-btn { width: 100%; padding: 20px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
.restart-btn { background: var(--success); color: white; padding: 20px; border-radius: 15px; border: none; font-size: 20px; font-weight: bold; cursor: pointer; width: 100%; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }
.opt-btn { padding: 15px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-gray); color: var(--text-main); cursor: pointer; font-weight: bold; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; }
.modal-box { background: var(--card-bg); padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.k-btn { padding: 15px 5px; background: var(--primary-blue); border: none; color: white; border-radius: 12px; font-weight: bold; cursor: pointer; }
#pin-display { font-size: 32px; letter-spacing: 8px; margin-bottom: 20px; height: 40px; color: var(--primary-blue); font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:30px; font-size:11px; background:none; border:none; color:#bbb; cursor:pointer; text-decoration: underline;">Alle Statistiken löschen</button>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option>
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
<option value="Scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label id="race-label">Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group" id="break-mode-container" style="display:none;">
<label>Anstoß-Modus:</label>
<select id="set-break">
<option value="wechsel">Wechselbreak</option>
<option value="winner">Winnerbreak</option>
</select>
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.onPlayerCountChange()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
<div class="settings-grid">
<button class="opt-btn" onclick="app.toggleDarkMode()">🌓 Dark Mode umschalten</button>
<button class="opt-btn" onclick="app.toggleFS()">⛶ Vollbild Modus</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px; font-size:20px; text-transform: uppercase;"></div>
<div id="sb-container"></div>
<div id="winner-area" style="max-width: 400px; margin: 20px auto;"></div>
<div id="controls-area"></div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Kugeln auf dem Tisch?</h3>
<button id="foul-toggle-btn" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-bottom:20px; font-weight:bold; background:none; cursor:pointer;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-top:20px; font-weight:bold; background:none; cursor:pointer; text-transform: uppercase;">Abbrechen</button>
</div>
</div>
<div id="restart-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Partie neustarten?</h3>
<p>Möchtest du den aktuellen Spielstand wirklich zurücksetzen?</p>
<button onclick="app.confirmRestart()" class="restart-btn" style="margin-top:10px;">JA, NEUSTART</button>
<button onclick="app.closeRestartPopup()" style="width:100%; padding:15px; border:2px solid #ccc; color:#666; border-radius:15px; margin-top:10px; font-weight:bold; background:none; cursor:pointer;">ABBRECHEN</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Wer beginnt?</h3>
<p>Bitte Spieler für den Anstoß wählen:</p>
<div id="break-choice-btns"></div>
</div>
</div>
<div id="pin-popup" class="modal-overlay">
<div class="modal-box">
<h3 id="pin-title" style="margin-top:0;">PIN eingeben</h3>
<div id="pin-display"></div>
<div class="kugel-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="k-btn" onclick="app.pressPin(1)">1</button>
<button class="k-btn" onclick="app.pressPin(2)">2</button>
<button class="k-btn" onclick="app.pressPin(3)">3</button>
<button class="k-btn" onclick="app.pressPin(4)">4</button>
<button class="k-btn" onclick="app.pressPin(5)">5</button>
<button class="k-btn" onclick="app.pressPin(6)">6</button>
<button class="k-btn" onclick="app.pressPin(7)">7</button>
<button class="k-btn" onclick="app.pressPin(8)">8</button>
<button class="k-btn" onclick="app.pressPin(9)">9</button>
<button class="k-btn" style="background:#888;" onclick="app.clearPin()">C</button>
<button class="k-btn" onclick="app.pressPin(0)">0</button>
<button class="k-btn" style="background:var(--danger);" onclick="app.closePinPopup()">X</button>
</div>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
correctPin: "12345", currentPin: "", pinAction: null,
wakeLock: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
// WAKE LOCK INTEGRATION
this.requestWakeLock();
document.addEventListener('visibilitychange', () => {
if (this.wakeLock !== null && document.visibilityState === 'visible') this.requestWakeLock();
});
},
async requestWakeLock() {
try {
if ('wakeLock' in navigator) {
this.wakeLock = await navigator.wakeLock.request('screen');
}
} catch (err) { console.log("WakeLock error"); }
},
toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
},
toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen();
}
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
openPinPopup(action, title) {
this.currentPin = "";
this.pinAction = action;
document.getElementById('pin-display').innerText = "";
document.getElementById('pin-title').innerText = title || "PIN eingeben";
document.getElementById('pin-popup').style.display = 'flex';
},
closePinPopup() { document.getElementById('pin-popup').style.display = 'none'; },
clearPin() { this.currentPin = ""; document.getElementById('pin-display').innerText = ""; },
pressPin(num) {
this.currentPin += num;
document.getElementById('pin-display').innerText = "*".repeat(this.currentPin.length);
if (this.currentPin === this.correctPin) {
const action = this.pinAction;
this.closePinPopup();
action();
} else if (this.currentPin.length >= this.correctPin.length) {
setTimeout(() => { alert("Falscher PIN"); this.clearPin(); }, 100);
}
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
return `<div class="player-db-entry">
<div class="p-header">
<span style="font-weight:bold; font-size:18px;">${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">Löschen</button>
</div>
<div class="p-stats">Bilanz: Siege: ${s.w} | Niederlagen: ${s.l} | HS: ${s.hs || 0}</div>
</div>`;
}).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) {
this.openPinPopup(() => {
this.players.push(n);
this.saveDB();
this.renderPlayerList();
document.getElementById('in-name').value="";
}, "Spieler hinzufügen");
}
},
delPlayer(i) {
this.openPinPopup(() => {
this.players.splice(i,1);
this.saveDB();
this.renderPlayerList();
}, "Spieler löschen");
},
resetStats() {
this.openPinPopup(() => {
this.stats = {};
this.saveDB();
this.renderPlayerList();
}, "Alle Statistiken löschen");
},
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for (let i = 1; i <= max; i++)
pSelect.innerHTML += `<option value="${i}" ${i === 2 ? 'selected' : ''}>${i} Spieler</option>`;
// 🟦 KOMFORT-FIX:
const ballGames = ["8-Ball", "9-Ball", "10-Ball"];
if (ballGames.includes(d)) {
// wenn vorher 1 Spieler war → automatisch auf 2 setzen
document.getElementById('set-pcount').value = 2;
}
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(disc);
const breakContainer = document.getElementById('break-mode-container');
if (isBallGame && pCount === 1) breakContainer.style.display = 'none';
else if (isBallGame) breakContainer.style.display = 'block';
else breakContainer.style.display = 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
let defVal = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}:</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
}
},
startGame() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
// FIX: Spielerzahl prüfen bei 8/9/10-Ball
const ballGames = ["8-Ball", "9-Ball", "10-Ball"];
if (ballGames.includes(disc) && names.length < 2) {
alert("Für diese Disziplin werden mindestens 2 Spieler benötigt!");
return;
}
const breakMode = document.getElementById('set-break').value;
this.game = {
disc, names,
scores: Array(names.length).fill(0),
activeIdx: 0,
balls: 15,
race: parseInt(document.getElementById('set-race').value),
breakMode,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })),
over: false,
recorded: false
};
this.history = [];
this.renderGame();
this.nav('game');
},
renderGame() {
const g = this.game;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isFreeSB = g.disc === 'Scoreboard';
const isSinglePlayer = g.names.length === 1;
document.getElementById('game-title').innerText = isFreeSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
const winnerArea = document.getElementById('winner-area');
container.innerHTML = ""; controls.innerHTML = ""; winnerArea.innerHTML = "";
let winnerIdx = !isFreeSB ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
if (isFreeSB) {
container.className = "free-sb-grid";
const cols = g.names.length < 3 ? g.names.length : 3;
container.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
} else {
container.className = "sb-layout";
}
g.names.forEach((name, i) => {
const stats = g.stats[i];
const isW = i === winnerIdx;
let statsHtml = (isBallGame || isFreeSB) ? "" : `
<div class="stats-row">Aufnahme: ${stats.innings} | GD: ${(g.scores[i] / stats.innings).toFixed(2)}</div>
<div class="stats-row">Serie: ${stats.currentSeries} | HS: ${stats.hs}</div>`;
const showFrame = !isSinglePlayer && ((isBallGame || !isFreeSB) && g.activeIdx === i);
const card = `
<div class="player-wrapper">
<div class="player-card ${showFrame ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}">
<div style="font-weight:bold; font-size:1.2rem;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${statsHtml}
</div>
${(isBallGame || isFreeSB) ? `
<div class="player-controls-mini">
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, -1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>
` : ''}
</div>`;
container.innerHTML += card;
if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
if (isBallGame || isFreeSB) {
controls.innerHTML = `<div class="controls-row-bottom"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button></div>`;
} else {
controls.innerHTML = `<div class="controls-row"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button><button class="btn-round" onclick="app.changeScore141(-1)" ${g.over?'disabled':''}>–</button><button class="btn-round" onclick="app.changeScore141(1)" ${g.over?'disabled':''}>+</button><button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button><button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button><button class="btn-round" style="color:#f1c40f;" onclick="app.triggerFoulLogic()" ${g.over?'disabled':''}>⚡</button></div>`;
}
if(g.over) winnerArea.innerHTML = `<button class="restart-btn" onclick="app.handleRematch()">REMATCH STARTEN</button>`;
},
handleRematch() {
const g = this.game;
const ballGames = ["8-Ball", "9-Ball", "10-Ball"];
// Robustheits-Check: Disziplin muss Ballgame sein + mind. 2 Spieler
if (ballGames.includes(g.disc) && g.names && g.names.length >= 2) {
this.openBreakChoicePopup();
return;
}
// Falls Ballgame aber zu wenig Spieler → Setup öffnen
if (ballGames.includes(g.disc) && g.names.length < 2) {
alert("Für diese Disziplin werden mindestens 2 Spieler benötigt!");
this.nav('setup');
return;
}
// Standard-Rematch
this.startGame();
},
openBreakChoicePopup() {
const popup = document.getElementById('break-choice-popup');
const btnArea = document.getElementById('break-choice-btns');
btnArea.innerHTML = "";
this.game.names.forEach((name, idx) => {
const btn = document.createElement('button');
btn.className = 'primary-btn';
btn.style.marginTop = '10px';
btn.innerText = name;
btn.onclick = () => { popup.style.display = 'none'; this.startGame(); this.game.activeIdx = idx; this.renderGame(); };
btnArea.appendChild(btn);
});
popup.style.display = 'flex';
},
changeScoreDir(idx, v) {
this.saveStep();
this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc);
if (isBallGame && v === 1 && this.game.names.length > 1) {
if (this.game.breakMode === 'wechsel') this.game.activeIdx = (this.game.activeIdx === 0) ? 1 : 0;
else if (this.game.breakMode === 'winner') this.game.activeIdx = idx;
}
this.renderGame();
},
changeScore141(v) {
const p = this.game.activeIdx;
if(v < 0) {
if (this.game.balls >= 15) return;
this.saveStep();
this.game.scores[p] += v;
this.game.balls++;
this.game.stats[p].currentSeries = Math.max(0, this.game.stats[p].currentSeries - 1);
} else {
this.saveStep();
this.game.scores[p] += v;
this.game.stats[p].currentSeries++;
if (this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.renderGame();
},
rack141() {
this.saveStep();
const p = this.game.activeIdx;
const pts = (this.game.balls - 1);
this.game.scores[p] += pts;
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15;
this.renderGame();
},
switchPlayer() {
this.saveStep();
this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++;
this.renderGame();
},
triggerFoulLogic() {
this.saveStep();
const pIdx = this.game.activeIdx;
const stats = this.game.stats[pIdx];
if (pIdx === 0 && stats.innings === 1 && stats.currentSeries === 0) this.game.scores[pIdx] -= 2;
else {
this.game.scores[pIdx] -= 1;
this.game.stats[pIdx].currentSeries = 0;
if (this.game.names.length > 1) { this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length; this.game.stats[this.game.activeIdx].innings++; }
else stats.innings++;
}
this.renderGame();
},
openKugelPopup() {
this.foulActive = false;
const foulBtn = document.getElementById('foul-toggle-btn');
if (foulBtn) { foulBtn.style.color = "#50a1e4"; foulBtn.style.borderColor = "#50a1e4"; }
const grid = document.getElementById('kugel-grid');
grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'k-btn'; b.innerText = i;
if (i > this.game.balls) { b.disabled = true; b.style.opacity = "0.3"; }
else {
b.onclick = () => {
this.saveStep();
let pts = (this.game.balls - i);
const p = this.game.activeIdx;
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
} else this.game.stats[p].currentSeries = 0;
this.game.balls = i <= 1 ? 15 : i;
this.closeKugelPopup();
this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; this.foulActive = false; },
toggleFoulFrame() { this.foulActive = !this.foulActive; const btn = document.getElementById('foul-toggle-btn'); btn.style.color = this.foulActive ? "red" : "#50a1e4"; btn.style.borderColor = this.foulActive ? "red" : "#50a1e4"; },
openRestartPopup() { document.getElementById('restart-popup').style.display = 'flex'; },
closeRestartPopup() { document.getElementById('restart-popup').style.display = 'none'; },
confirmRestart() { this.closeRestartPopup(); this.startGame(); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
if(wIdx === -1) return;
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if (this.game.stats[i].hs > (this.stats[n].hs || 0)) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
}
};
app.init();
</script>
</body>
</html>
- AIMhAK
- Pomeranzenkiller

- Beiträge: 63
- Registriert: 24.10.12 19:34
- Reputation: 24
- Name: Aaron
- Playing cue: Mezz EC7-C + ExPro
- Tip: G2 Medium
- Break Cue: Mezz Power Break Kai PBKW-T
- Jump Cue: Predator Air 2 Jump
- Wohnort: Alsheim
Re: Scoreboard (HTML)
Mit beiden Codes wird auch unter Verwendung des Fully Kiosk Browsers kein Popup zur Abfrage des Anstoßrechts geöffnet.
Andere Popups funktionieren allerdings.
Wie ich bisher immer vorgegangen bin:
HTML-Code vollständig in Editor kopiert, als .txt gespeichert und in .html geändert.
Habe in den verschiedenen Browsern auch immer wieder den Cache geleert, bevor ich eine neue Version geöffnet habe.
Andere Popups funktionieren allerdings.
Wie ich bisher immer vorgegangen bin:
HTML-Code vollständig in Editor kopiert, als .txt gespeichert und in .html geändert.
Habe in den verschiedenen Browsern auch immer wieder den Cache geleert, bevor ich eine neue Version geöffnet habe.
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
Dann ist das was anderes bei dir. Habe den Code nun auf ne Webseite hochgeladen, der lauft da auch.
Sicher. das da kein PopUp Blocker aktiv ist?
Probiere mal den:
Sicher. das da kein PopUp Blocker aktiv ist?
Probiere mal den:
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--app-bg: #e9ecef;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
}
body.dark-mode {
--bg-gray: #2c2c2c;
--app-bg: #121212;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
body {
font-family: 'Segoe UI', sans-serif;
background-color: var(--card-bg);
margin: 0;
padding: 0;
display: block;
transition: 0.3s;
color: var(--text-main);
}
#app {
width: 100%;
max-width: 100%;
background: var(--card-bg);
border-radius: 0;
overflow: hidden;
box-shadow: none;
min-height: 100vh;
position: relative;
}
.nav-tabs { display: flex; background: var(--card-bg); border-bottom: 2px solid var(--border-color); align-items: center; }
.tab-link { flex: 1; padding: 20px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; transition: 0.3s; border-bottom: 4px solid transparent; }
.tab-link.active { color: var(--primary-blue); border-bottom: 4px solid var(--primary-blue); background-color: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 30px; }
.content-section.active { display: block; }
.player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid var(--border-color); }
.p-header { display: flex; justify-content: space-between; align-items: center; }
.p-stats { font-size: 14px; color: #777; margin-top: 4px; }
body.dark-mode .p-stats { color: #aaa; }
.del-btn { background: var(--primary-blue); color: white; border: none; padding: 8px 20px; border-radius: 12px; cursor: pointer; font-weight: bold; }
.setup-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: var(--text-label); }
input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid var(--border-color); font-size: 16px; box-sizing: border-box; background: var(--card-bg); color: var(--text-main); }
.sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
.free-sb-grid { display: grid; gap: 25px; margin-top: 20px; width: 100%; }
.player-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; width: 100%; }
.player-card {
background: var(--primary-blue);
color: #fff;
padding: 30px 20px;
border-radius: 35px;
text-align: center;
border: 6px solid transparent;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
box-shadow: 0 10px 25px rgba(80,161,228,0.2);
}
.player-card.active-turn { border-color: var(--text-main); border-width: 6px; transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: 80px; font-weight: 800; margin: 5px 0; line-height: 0.9; }
.stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; font-weight: 500; }
.controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
.controls-row-bottom { display: flex; justify-content: center; gap: 20px; margin-top: 40px; padding-bottom: 30px; }
.player-controls-mini { display: flex; gap: 15px; justify-content: center; }
.btn-round { width: 70px; height: 70px; border-radius: 50%; border: 2px solid var(--border-color); background: var(--card-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.05); font-size: 30px; color: var(--primary-blue); transition: 0.2s; }
.btn-round-small { width: 55px; height: 55px; border-radius: 50%; border: 1px solid var(--border-color); background: var(--card-bg); font-size: 24px; color: var(--primary-blue); cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-round:active, .btn-round-small:active { transform: scale(0.9); }
.btn-swap-small { background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; color: var(--text-main); }
.primary-btn { width: 100%; padding: 20px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }
.restart-btn { background: var(--success); color: white; padding: 20px; border-radius: 15px; border: none; font-size: 20px; font-weight: bold; cursor: pointer; width: 100%; }
.settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; }
.opt-btn { padding: 15px; border: 1px solid var(--border-color); border-radius: 12px; background: var(--bg-gray); color: var(--text-main); cursor: pointer; font-weight: bold; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; justify-content: center; align-items: center; }
.modal-box { background: var(--card-bg); padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
.kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
.k-btn { padding: 15px 5px; background: var(--primary-blue); border: none; color: white; border-radius: 12px; font-weight: bold; cursor: pointer; }
#pin-display { font-size: 32px; letter-spacing: 8px; margin-bottom: 20px; height: 40px; color: var(--primary-blue); font-weight: bold; }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">1. Spieler</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">2. Setup</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">3. Scoreboard</div>
</div>
<div id="sec-settings" class="content-section active">
<h2>Spieler-Datenbank</h2>
<div style="display: flex; gap: 10px; margin-bottom: 25px;">
<input type="text" id="in-name" placeholder="Name eingeben...">
<button onclick="app.addPlayer()" style="background:var(--primary-blue); color:white; border:none; padding:0 30px; border-radius:12px; cursor:pointer; font-weight:bold;">Hinzufügen</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:30px; font-size:11px; background:none; border:none; color:#bbb; cursor:pointer; text-decoration: underline;">Alle Statistiken löschen</button>
</div>
<div id="sec-setup" class="content-section">
<h2>Partie-Einstellungen</h2>
<div class="setup-group">
<label>Disziplin:</label>
<select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option>
<option value="8-Ball">8-Ball</option>
<option value="9-Ball">9-Ball</option>
<option value="10-Ball">10-Ball</option>
<option value="Scoreboard">Freies Scoreboard</option>
</select>
</div>
<div class="setup-group" id="race-container">
<label id="race-label">Race To:</label>
<input type="number" id="set-race" value="100">
</div>
<div class="setup-group" id="break-mode-container" style="display:none;">
<label>Anstoß-Modus:</label>
<select id="set-break">
<option value="wechsel">Wechselbreak</option>
<option value="winner">Winnerbreak</option>
</select>
</div>
<div class="setup-group">
<label>Anzahl Spieler:</label>
<select id="set-pcount" onchange="app.onPlayerCountChange()"></select>
</div>
<div id="player-select-area"></div>
<button onclick="app.handleStartCheck()" class="primary-btn">SPIEL STARTEN</button>
<div class="settings-grid">
<button class="opt-btn" onclick="app.toggleDarkMode()">🌓 Dark Mode umschalten</button>
<button class="opt-btn" onclick="app.toggleFS()">⛶ Vollbild Modus</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px; font-size:20px; text-transform: uppercase;"></div>
<div id="sb-container"></div>
<div id="winner-area" style="max-width: 400px; margin: 20px auto;"></div>
<div id="controls-area"></div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Kugeln auf dem Tisch?</h3>
<button id="foul-toggle-btn" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-bottom:20px; font-weight:bold; background:none; cursor:pointer;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div class="kugel-grid" id="kugel-grid"></div>
<button onclick="app.closeKugelPopup()" style="width:100%; padding:15px; border:2px solid #50a1e4; color:#50a1e4; border-radius:15px; margin-top:20px; font-weight:bold; background:none; cursor:pointer; text-transform: uppercase;">Abbrechen</button>
</div>
</div>
<div id="restart-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Partie neustarten?</h3>
<p>Möchtest du den aktuellen Spielstand wirklich zurücksetzen?</p>
<button onclick="app.confirmRestart()" class="restart-btn" style="margin-top:10px;">JA, NEUSTART</button>
<button onclick="app.closeRestartPopup()" style="width:100%; padding:15px; border:2px solid #ccc; color:#666; border-radius:15px; margin-top:10px; font-weight:bold; background:none; cursor:pointer;">ABBRECHEN</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box">
<h3 style="margin-top:0;">Wer beginnt?</h3>
<p>Bitte Spieler für den Anstoß wählen:</p>
<div id="break-choice-btns"></div>
</div>
</div>
<div id="pin-popup" class="modal-overlay">
<div class="modal-box">
<h3 id="pin-title" style="margin-top:0;">PIN eingeben</h3>
<div id="pin-display"></div>
<div class="kugel-grid" style="grid-template-columns: repeat(3, 1fr);">
<button class="k-btn" onclick="app.pressPin(1)">1</button>
<button class="k-btn" onclick="app.pressPin(2)">2</button>
<button class="k-btn" onclick="app.pressPin(3)">3</button>
<button class="k-btn" onclick="app.pressPin(4)">4</button>
<button class="k-btn" onclick="app.pressPin(5)">5</button>
<button class="k-btn" onclick="app.pressPin(6)">6</button>
<button class="k-btn" onclick="app.pressPin(7)">7</button>
<button class="k-btn" onclick="app.pressPin(8)">8</button>
<button class="k-btn" onclick="app.pressPin(9)">9</button>
<button class="k-btn" style="background:#888;" onclick="app.clearPin()">C</button>
<button class="k-btn" onclick="app.pressPin(0)">0</button>
<button class="k-btn" style="background:var(--danger);" onclick="app.closePinPopup()">X</button>
</div>
</div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
correctPin: "12345", currentPin: "", pinAction: null,
wakeLock: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
this.requestWakeLock();
},
async requestWakeLock() {
try { if ('wakeLock' in navigator) this.wakeLock = await navigator.wakeLock.request('screen'); }
catch (err) { console.log("WakeLock error"); }
},
toggleDarkMode() {
const isDark = document.body.classList.toggle('dark-mode');
localStorage.setItem('darkMode', isDark);
},
toggleFS() {
if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(err => console.log(err));
else document.exitFullscreen();
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
},
openPinPopup(action, title) {
this.currentPin = ""; this.pinAction = action;
document.getElementById('pin-display').innerText = "";
document.getElementById('pin-title').innerText = title || "PIN eingeben";
document.getElementById('pin-popup').style.display = 'flex';
},
closePinPopup() { document.getElementById('pin-popup').style.display = 'none'; },
clearPin() { this.currentPin = ""; document.getElementById('pin-display').innerText = ""; },
pressPin(num) {
this.currentPin += num;
document.getElementById('pin-display').innerText = "*".repeat(this.currentPin.length);
if (this.currentPin === this.correctPin) {
const action = this.pinAction; this.closePinPopup(); action();
} else if (this.currentPin.length >= this.correctPin.length) {
setTimeout(() => { alert("Falscher PIN"); this.clearPin(); }, 100);
}
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
ui.innerHTML = this.players.map((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
return `<div class="player-db-entry">
<div class="p-header">
<span style="font-weight:bold; font-size:18px;">${p}</span>
<button class="del-btn" onclick="app.delPlayer(${i})">Löschen</button>
</div>
<div class="p-stats">Bilanz: Siege: ${s.w} | Niederlagen: ${s.l} | HS: ${s.hs || 0}</div>
</div>`;
}).join('');
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) {
this.openPinPopup(() => {
this.players.push(n); this.saveDB(); this.renderPlayerList();
document.getElementById('in-name').value="";
}, "Spieler hinzufügen");
}
},
delPlayer(i) {
this.openPinPopup(() => {
this.players.splice(i,1); this.saveDB(); this.renderPlayerList();
}, "Spieler löschen");
},
resetStats() {
this.openPinPopup(() => {
this.stats = {}; this.saveDB(); this.renderPlayerList();
}, "Alle Statistiken löschen");
},
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for (let i = 1; i <= max; i++) pSelect.innerHTML += `<option value="${i}" ${i === 2 ? 'selected' : ''}>${i} Spieler</option>`;
if (["8-Ball", "9-Ball", "10-Ball"].includes(d)) document.getElementById('set-pcount').value = 2;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(disc);
document.getElementById('break-mode-container').style.display = (isBallGame && pCount > 1) ? 'block' : 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const count = document.getElementById('set-pcount').value;
const area = document.getElementById('player-select-area');
area.innerHTML = "";
for(let i=0; i<count; i++) {
let defVal = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}:</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
}
},
handleStartCheck() {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
if (["8-Ball", "9-Ball", "10-Ball"].includes(disc) && names.length >= 2) {
this.openBreakChoicePopup();
} else {
this.startGame();
}
},
startGame(starterIdx = 0) {
const disc = document.getElementById('set-disc').value;
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
if (["8-Ball", "9-Ball", "10-Ball"].includes(disc) && names.length < 2) {
alert("Für diese Disziplin werden mindestens 2 Spieler benötigt!"); return;
}
this.game = {
disc, names,
scores: Array(names.length).fill(0),
activeIdx: starterIdx,
balls: 15,
race: parseInt(document.getElementById('set-race').value),
breakMode: document.getElementById('set-break').value,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })),
over: false, recorded: false
};
this.history = []; this.renderGame(); this.nav('game');
},
renderGame() {
const g = this.game;
const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isFreeSB = g.disc === 'Scoreboard';
document.getElementById('game-title').innerText = isFreeSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
const winnerArea = document.getElementById('winner-area');
container.innerHTML = ""; controls.innerHTML = ""; winnerArea.innerHTML = "";
let winnerIdx = !isFreeSB ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
container.className = isFreeSB ? "free-sb-grid" : "sb-layout";
if (isFreeSB) container.style.gridTemplateColumns = `repeat(${Math.min(g.names.length, 3)}, 1fr)`;
g.names.forEach((name, i) => {
const stats = g.stats[i];
const isW = i === winnerIdx;
const showFrame = g.activeIdx === i && g.names.length > 1;
let statsHtml = (isBallGame || isFreeSB) ? "" : `<div class="stats-row">Aufn: ${stats.innings} | Serie: ${stats.currentSeries} | HS: ${stats.hs}</div>`;
container.innerHTML += `
<div class="player-wrapper">
<div class="player-card ${showFrame ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}">
<div style="font-weight:bold; font-size:1.2rem;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${statsHtml}
</div>
${(isBallGame || isFreeSB) ? `
<div class="player-controls-mini">
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, -1)" ${g.over?'disabled':''}>–</button>
<button class="btn-round-small" onclick="app.changeScoreDir(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>` : ''}
</div>`;
if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
if (isBallGame || isFreeSB) {
controls.innerHTML = `<div class="controls-row-bottom"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button></div>`;
} else {
controls.innerHTML = `<div class="controls-row"><button class="btn-round" onclick="app.openRestartPopup()">↺</button><button class="btn-round" onclick="app.undo()">↶</button><button class="btn-round" onclick="app.changeScore141(-1)" ${g.over?'disabled':''}>–</button><button class="btn-round" onclick="app.changeScore141(1)" ${g.over?'disabled':''}>+</button><button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button><button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button><button class="btn-round" style="color:#f1c40f;" onclick="app.triggerFoulLogic()" ${g.over?'disabled':''}>⚡</button></div>`;
}
if(g.over) winnerArea.innerHTML = `<button class="restart-btn" onclick="app.handleRematch()">REMATCH STARTEN</button>`;
},
handleRematch() {
if (["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc)) this.openBreakChoicePopup();
else this.startGame();
},
openBreakChoicePopup() {
const popup = document.getElementById('break-choice-popup');
const btnArea = document.getElementById('break-choice-btns');
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
btnArea.innerHTML = "";
names.forEach((name, idx) => {
const btn = document.createElement('button');
btn.className = 'primary-btn'; btn.style.marginTop = '10px'; btn.innerText = name;
btn.onclick = () => { popup.style.display = 'none'; this.startGame(idx); };
btnArea.appendChild(btn);
});
popup.style.display = 'flex';
},
changeScoreDir(idx, v) {
this.saveStep();
this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
if (["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc) && v === 1 && this.game.names.length > 1) {
if (this.game.breakMode === 'wechsel') this.game.activeIdx = (this.game.activeIdx === 0) ? 1 : 0;
else if (this.game.breakMode === 'winner') this.game.activeIdx = idx;
}
this.renderGame();
},
changeScore141(v) {
const p = this.game.activeIdx; this.saveStep();
if(v < 0) {
if (this.game.balls >= 15) return;
this.game.scores[p] += v; this.game.balls++;
this.game.stats[p].currentSeries = Math.max(0, this.game.stats[p].currentSeries - 1);
} else {
this.game.scores[p] += v; this.game.stats[p].currentSeries++;
if (this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.renderGame();
},
rack141() {
this.saveStep(); const p = this.game.activeIdx; const pts = (this.game.balls - 1);
this.game.scores[p] += pts; this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15; this.renderGame();
},
switchPlayer() {
this.saveStep(); this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++; this.renderGame();
},
triggerFoulLogic() {
this.saveStep(); const pIdx = this.game.activeIdx;
if (pIdx === 0 && this.game.stats[pIdx].innings === 1 && this.game.stats[pIdx].currentSeries === 0) this.game.scores[pIdx] -= 2;
else this.game.scores[pIdx] -= 1;
this.switchPlayer();
},
openKugelPopup() {
this.foulActive = false; const grid = document.getElementById('kugel-grid'); grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'k-btn'; b.innerText = i;
if (i > this.game.balls) { b.disabled = true; b.style.opacity = "0.3"; }
else {
b.onclick = () => {
this.saveStep(); let pts = (this.game.balls - i); const p = this.game.activeIdx;
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.game.balls = i <= 1 ? 15 : i; this.closeKugelPopup(); this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; },
toggleFoulFrame() { this.foulActive = !this.foulActive; const btn = document.getElementById('foul-toggle-btn'); btn.style.color = this.foulActive ? "red" : "#50a1e4"; btn.style.borderColor = this.foulActive ? "red" : "#50a1e4"; },
openRestartPopup() { document.getElementById('restart-popup').style.display = 'flex'; },
closeRestartPopup() { document.getElementById('restart-popup').style.display = 'none'; },
confirmRestart() { this.closeRestartPopup(); this.startGame(this.game.activeIdx); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if (this.game.stats[i].hs > (this.stats[n].hs || 0)) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
}
};
app.init();
</script>
</body>
</html>- AIMhAK
- Pomeranzenkiller

- Beiträge: 63
- Registriert: 24.10.12 19:34
- Reputation: 24
- Name: Aaron
- Playing cue: Mezz EC7-C + ExPro
- Tip: G2 Medium
- Break Cue: Mezz Power Break Kai PBKW-T
- Jump Cue: Predator Air 2 Jump
- Wohnort: Alsheim
Re: Scoreboard (HTML)
Mit dem letzten Code funktioniert es.
Vielen Dank!
Vielen Dank!
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
Aktueller Code sollte nun auch auch einem Smartphone lesbar sein
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
--modal-overlay: rgba(0,0,0,0.8);
}
body.dark-mode {
--bg-gray: #2c2c2c;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
body {
font-family: 'Segoe UI', Roboto, sans-serif;
background-color: var(--card-bg);
margin: 0; padding: 0; color: var(--text-main);
overflow-x: hidden;
}
#app { width: 100%; min-height: 100vh; display: flex; flex-direction: column; }
.nav-tabs {
display: flex;
background: var(--card-bg);
border-bottom: 2px solid var(--border-color);
position: sticky; top: 0; z-index: 100;
}
.tab-link { flex: 1; padding: 15px 5px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; border-bottom: 4px solid transparent; font-size: 14px; }
.tab-link.active { color: var(--primary-blue); border-bottom-color: var(--primary-blue); background: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 15px; flex: 1; width: 100%; box-sizing: border-box; }
.content-section.active { display: block; }
.table-container { width: 100%; overflow-x: auto; }
.stats-table { width: 100%; border-collapse: collapse; background: var(--bg-gray); border-radius: 12px; }
.stats-table th, .stats-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid var(--border-color); }
.stats-table th { background: var(--primary-blue); color: white; font-size: 12px; }
.setup-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: var(--text-label); font-size: 14px; }
input, select { width: 100%; padding: 12px; border-radius: 10px; border: 1px solid var(--border-color); background: var(--card-bg); color: var(--text-main); font-size: 16px; }
/* Scoreboard Layout */
.sb-layout { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin-top: 10px; }
.player-container { flex: 1 1 300px; max-width: 500px; display: flex; flex-direction: column; align-items: center; }
.player-card {
width: 100%; background: var(--primary-blue); color: white; padding: 25px 15px; border-radius: 25px;
text-align: center; border: 6px solid transparent; transition: 0.3s; box-sizing: border-box;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.player-card.active-turn { border-color: var(--text-main); transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: clamp(60px, 15vw, 100px); font-weight: 800; line-height: 1; margin: 10px 0; }
/* Buttons & Controls */
.controls-wrapper { display: flex; justify-content: center; flex-wrap: wrap; gap: 12px; margin-top: 30px; padding-bottom: 20px; }
.btn-round {
width: 65px; height: 65px; border-radius: 50%; border: 1px solid var(--border-color);
background: var(--card-bg); cursor: pointer; font-size: 24px; color: var(--primary-blue);
display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.primary-btn { width: 100%; padding: 16px; border: none; border-radius: 12px; font-size: 16px; font-weight: bold; cursor: pointer; }
.primary-btn.blue { background: var(--primary-blue); color: white; }
.primary-btn.green { background: var(--success); color: white; }
.primary-btn.gray { background: #777; color: white; }
.primary-btn.cancel { background: #333; color: #ccc; border: 1px solid #444; }
/* Modals */
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--modal-overlay); z-index: 1000; justify-content: center; align-items: center; padding: 20px; }
.modal-box { background: #1e1e1e; color: white; padding: 25px; border-radius: 25px; width: 100%; max-width: 380px; text-align: center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
@media (max-width: 600px) { .sb-layout { flex-direction: column; } }
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">SPIELER</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">SETUP</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">SCORE</div>
</div>
<div id="sec-settings" class="content-section active">
<div style="display: flex; gap: 8px; margin-bottom: 15px;">
<input type="text" id="in-name" placeholder="Name..." style="flex:1;">
<button onclick="app.addPlayer()" class="primary-btn blue" style="width: auto; padding: 0 20px;">+</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:20px; color: grey; background: none; border: none; text-decoration: underline; font-size: 12px; width: 100%;">Statistiken zurücksetzen</button>
</div>
<div id="sec-setup" class="content-section">
<div class="setup-group"><label>Disziplin</label><select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option><option value="8-Ball">8-Ball</option><option value="9-Ball">9-Ball</option><option value="10-Ball">10-Ball</option><option value="Scoreboard">Scoreboard</option>
</select></div>
<div class="setup-group" id="race-container"><label>Race To</label><input type="number" id="set-race" value="100"></div>
<div class="setup-group" id="break-mode-container" style="display:none;"><label>Anstoß</label><select id="set-break"><option value="wechsel">Wechselbreak</option><option value="winner">Winnerbreak</option></select></div>
<div class="setup-group"><label>Spieleranzahl</label><select id="set-pcount" onchange="app.onPlayerCountChange()"></select></div>
<div id="player-select-area"></div>
<button onclick="app.handleStartCheck()" class="primary-btn green" style="margin-top: 15px;">SPIEL STARTEN</button>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
<button onclick="app.toggleDarkMode()" class="primary-btn gray">DESIGN</button>
<button onclick="app.toggleFS()" class="primary-btn gray">VOLLBILD</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:15px;"></div>
<div id="sb-container" class="sb-layout"></div>
<div id="winner-area"></div>
<div id="controls-area" class="controls-wrapper"></div>
</div>
<div id="confirm-restart-modal" class="modal-overlay">
<div class="modal-box">
<h3>Partie neustarten?</h3>
<p style="color:#bbb; margin-bottom:25px;">Aktuellen Spielstand wirklich zurücksetzen?</p>
<button class="primary-btn green" onclick="app.confirmRestart()" style="margin-bottom:12px;">JA, NEUSTART</button>
<button class="primary-btn cancel" onclick="app.closeModal('confirm-restart-modal')">ABBRECHEN</button>
</div>
</div>
<div id="kugel-popup" class="modal-overlay">
<div class="modal-box">
<h3>Kugeln am Tisch?</h3>
<button id="foul-toggle-btn" class="primary-btn" style="background:none; color:var(--primary-blue); border:2px solid var(--primary-blue); margin-bottom:15px;" onclick="app.toggleFoulFrame()">FOUL?</button>
<div id="kugel-grid" style="display:grid; grid-template-columns: repeat(4, 1fr); gap: 10px;"></div>
<button onclick="app.closeModal('kugel-popup')" class="primary-btn cancel" style="margin-top:15px;">Abbrechen</button>
</div>
</div>
<div id="break-choice-popup" class="modal-overlay">
<div class="modal-box"><h3>Wer beginnt?</h3><div id="break-choice-btns"></div></div>
</div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
if(id === 'game') this.renderGame();
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
let html = `<table class="stats-table"><thead><tr><th>Name</th><th>S/N</th><th>HS</th><th></th></tr></thead><tbody>`;
this.players.forEach((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
html += `<tr><td><strong>${p}</strong></td><td><span style="color:green">${s.w}</span>/<span style="color:red">${s.l}</span></td><td>${s.hs}</td>
<td style="text-align:right;"><button style="background:var(--danger); color:white; border:none; border-radius:5px; padding:5px 10px;" onclick="app.delPlayer(${i})">X</button></td></tr>`;
});
ui.innerHTML = html + `</tbody></table>`;
this.renderPlayerSelects();
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) { this.players.push(n); this.saveDB(); this.renderPlayerList(); document.getElementById('in-name').value = ""; }
},
delPlayer(i) { this.players.splice(i, 1); this.saveDB(); this.renderPlayerList(); },
resetStats() { if(confirm("Statistiken löschen?")) { this.stats = {}; this.saveDB(); this.renderPlayerList(); } },
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
document.getElementById('break-mode-container').style.display = (["8-Ball", "9-Ball", "10-Ball"].includes(disc) && pCount > 1) ? 'block' : 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const area = document.getElementById('player-select-area');
const count = document.getElementById('set-pcount').value;
area.innerHTML = "";
for(let i=0; i<count; i++) {
let def = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p===def?'selected':''}>${p}</option>`).join('')}</select></div>`;
}
},
handleStartCheck() {
const disc = document.getElementById('set-disc').value;
if (["8-Ball", "9-Ball", "10-Ball"].includes(disc)) this.openBreakChoicePopup();
else this.startGame();
},
startGame(starterIdx = 0) {
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
this.game = {
disc: document.getElementById('set-disc').value, names, scores: names.map(() => 0),
activeIdx: starterIdx, balls: 15, race: parseInt(document.getElementById('set-race').value) || 0,
breakMode: document.getElementById('set-break').value,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })), over: false, recorded: false
};
this.history = []; this.renderGame(); this.nav('game');
},
renderGame() {
const g = this.game; if(!g) return;
const isBall = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
const isSB = g.disc === "Scoreboard";
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
container.innerHTML = ""; controls.innerHTML = "";
let winnerIdx = (!isSB) ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if(g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
g.names.forEach((name, i) => {
const isTurn = g.activeIdx === i && g.names.length > 1;
container.innerHTML += `
<div class="player-container">
<div class="player-card ${isTurn?'active-turn':''} ${i===winnerIdx?'winner-card':''}">
<div style="font-weight:bold; font-size:18px;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
</div>
${(isBall || isSB) ? `<div style="display:flex; gap:20px; margin-top:15px;">
<button class="btn-round" onclick="app.addScore(${i}, -1)" ${g.over?'disabled':''}>-</button>
<button class="btn-round" onclick="app.addScore(${i}, 1)" ${g.over?'disabled':''}>+</button>
</div>` : ''}
</div>`;
if(!isBall && !isSB && i === 0 && g.names.length === 2) {
container.innerHTML += `<button class="btn-round" style="margin: auto 0; border:none; background:none; font-size:30px;" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
}
});
// Buttons unten
let btnHtml = `
<button class="btn-round" onclick="app.requestRestart()">↺</button>
<button class="btn-round" onclick="app.undo()">↶</button>
`;
if(!isBall && !isSB) {
btnHtml += `
<button class="btn-round" onclick="app.change141(-1)" ${g.over?'disabled':''}>-</button>
<button class="btn-round" onclick="app.change141(1)" ${g.over?'disabled':''}>+</button>
<button class="btn-round" onclick="app.rack141()" ${g.over?'disabled':''}>△</button>
<button class="btn-round" style="background:var(--primary-blue); color:white;" onclick="app.openKugelPopup()" ${g.over?'disabled':''}>${g.balls}</button>
<button class="btn-round" style="color:red;" onclick="app.foul141()" ${g.over?'disabled':''}>⚡</button>
`;
}
controls.innerHTML = btnHtml;
if(g.over) document.getElementById('winner-area').innerHTML = `<button class="primary-btn green" style="margin-top:20px;" onclick="app.handleRematch()">REMATCH</button>`;
else document.getElementById('winner-area').innerHTML = "";
document.getElementById('game-title').innerText = isSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
},
requestRestart() { document.getElementById('confirm-restart-modal').style.display = 'flex'; },
confirmRestart() { this.closeModal('confirm-restart-modal'); this.startGame(this.game.activeIdx); },
closeModal(id) { document.getElementById(id).style.display = 'none'; },
addScore(idx, v) {
this.saveStep();
this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
if(["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc) && v === 1) {
if(this.game.breakMode === 'wechsel') this.game.activeIdx = (idx === 0) ? 1 : 0;
else this.game.activeIdx = idx;
}
this.renderGame();
},
change141(v) {
const p = this.game.activeIdx; this.saveStep();
if(v > 0) {
this.game.scores[p]++; this.game.stats[p].currentSeries++;
if(this.game.balls <= 2) this.game.balls = 15; else this.game.balls--;
} else {
if(this.game.balls < 15) { this.game.scores[p]--; this.game.balls++; this.game.stats[p].currentSeries--; }
}
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.renderGame();
},
rack141() {
this.saveStep(); const p = this.game.activeIdx; const pts = (this.game.balls - 1);
this.game.scores[p] += pts; this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.game.balls = 15; this.renderGame();
},
switchPlayer() {
this.saveStep(); this.game.stats[this.game.activeIdx].currentSeries = 0;
this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length;
this.game.stats[this.game.activeIdx].innings++; this.renderGame();
},
foul141() {
this.saveStep(); const p = this.game.activeIdx;
this.game.scores[p] -= (this.game.stats[p].innings === 1 && this.game.stats[p].currentSeries === 0) ? 2 : 1;
this.switchPlayer();
},
openKugelPopup() {
this.foulActive = false; const grid = document.getElementById('kugel-grid'); grid.innerHTML = "";
for(let i=0; i<=15; i++) {
const b = document.createElement('button'); b.className = 'btn-round'; b.style.width="100%"; b.style.height="auto"; b.style.padding="10px 0"; b.style.borderRadius="10px"; b.innerText = i;
if(i > this.game.balls) { b.disabled = true; b.style.opacity = 0.3; }
else {
b.onclick = () => {
this.saveStep(); const p = this.game.activeIdx; let pts = (this.game.balls - i);
this.game.scores[p] += this.foulActive ? (pts - 1) : pts;
if(!this.foulActive) {
this.game.stats[p].currentSeries += pts;
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
}
this.game.balls = (i <= 1) ? 15 : i; this.closeModal('kugel-popup'); this.switchPlayer();
};
}
grid.appendChild(b);
}
document.getElementById('kugel-popup').style.display = 'flex';
},
toggleFoulFrame() { this.foulActive = !this.foulActive; document.getElementById('foul-toggle-btn').style.color = this.foulActive ? 'red' : 'var(--primary-blue)'; },
handleRematch() { if(["8-Ball", "9-Ball", "10-Ball"].includes(this.game.disc)) this.openBreakChoicePopup(); else this.startGame(); },
openBreakChoicePopup() {
const area = document.getElementById('break-choice-btns');
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
area.innerHTML = names.map((n, i) => `<button class="primary-btn green" style="margin-bottom:8px;" onclick="app.closeBreakPopup(${i})">${n}</button>`).join('');
document.getElementById('break-choice-popup').style.display = 'flex';
},
closeBreakPopup(idx) { document.getElementById('break-choice-popup').style.display = 'none'; this.startGame(idx); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if(this.game.stats[i].hs > this.stats[n].hs) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
},
toggleDarkMode() { const d = document.body.classList.toggle('dark-mode'); localStorage.setItem('darkMode', d); },
toggleFS() { if(!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); }
};
app.init();
</script>
</body>
</html>- AIMhAK
- Pomeranzenkiller

- Beiträge: 63
- Registriert: 24.10.12 19:34
- Reputation: 24
- Name: Aaron
- Playing cue: Mezz EC7-C + ExPro
- Tip: G2 Medium
- Break Cue: Mezz Power Break Kai PBKW-T
- Jump Cue: Predator Air 2 Jump
- Wohnort: Alsheim
Re: Scoreboard (HTML)
Der aktuelle Code funktioniert im Querformat perfekt auf dem Smartphone.
Im Hochformat sind beim Scoreboard die blauen Felder jetzt untereinander angeordnet, was grundsätzlich in Ordnung ist. Leider ist der Abstand zwischen den Feldern so groß, dass insbesondere beim 14.1 zum Bedienen der Buttons nach unten gescrollt werden muss, auch im Vollbildmodus:

Im Hochformat sind beim Scoreboard die blauen Felder jetzt untereinander angeordnet, was grundsätzlich in Ordnung ist. Leider ist der Abstand zwischen den Feldern so groß, dass insbesondere beim 14.1 zum Bedienen der Buttons nach unten gescrollt werden muss, auch im Vollbildmodus:

-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
ok.... ich gehe die tage das nochmal durch, das das design überall passt.
Funktionen fallen mir aktuell keine mehr ein, die man braucht.
Für Dinge wie Shotclock sehe ich keinen Bedarf, bei mir zumindest nicht.
Funktionen fallen mir aktuell keine mehr ein, die man braucht.
Für Dinge wie Shotclock sehe ich keinen Bedarf, bei mir zumindest nicht.
- AIMhAK
- Pomeranzenkiller

- Beiträge: 63
- Registriert: 24.10.12 19:34
- Reputation: 24
- Name: Aaron
- Playing cue: Mezz EC7-C + ExPro
- Tip: G2 Medium
- Break Cue: Mezz Power Break Kai PBKW-T
- Jump Cue: Predator Air 2 Jump
- Wohnort: Alsheim
Re: Scoreboard (HTML)
Shotclock finde ich auch überflüssig.
Was mir noch gefallen würde, wäre eine detaillierte Spielerstatistik:
- Spiele insgesamt
- S/N insgesamt
- Gewinnquote in Prozent insgesamt
- Spiele, "S/N" und Gewinnquote in Prozent je Disziplin
- Höchstserie 14.1
- Höchster Durchschnitt 14.1
- Gesamt-Durchschnitt 14.1
- Anzahl der Runouts bei 8-, 9- und 10-Ball
- Runout-Quote in Prozent
- Verlauf: Gegner/Disziplin/Ergebnis der letzten 5 Spiele
Weil das natürlich nicht alles in die Spielerliste passt, könnte es ja so gestaltet werden, dass in der Liste nur die ersten drei genannten angezeigt werden und sich bei einem Klick auf den jeweiligen Spielernamen ein Popup mit detaillierter Spielerstatistik öffnet.
Für das Erfassen von Runouts könnte es ja einen entsprechenden Button auf dem Scoreboard neben dem "+" geben, angelehnt eine solche Funktion beim Cuescore Scoreboard.
Aber das wären letztendlich nur Spielereien.
Grundsätzlich ist alles da, was man braucht. Vielen Dank dafür!
Was mir noch gefallen würde, wäre eine detaillierte Spielerstatistik:
- Spiele insgesamt
- S/N insgesamt
- Gewinnquote in Prozent insgesamt
- Spiele, "S/N" und Gewinnquote in Prozent je Disziplin
- Höchstserie 14.1
- Höchster Durchschnitt 14.1
- Gesamt-Durchschnitt 14.1
- Anzahl der Runouts bei 8-, 9- und 10-Ball
- Runout-Quote in Prozent
- Verlauf: Gegner/Disziplin/Ergebnis der letzten 5 Spiele
Weil das natürlich nicht alles in die Spielerliste passt, könnte es ja so gestaltet werden, dass in der Liste nur die ersten drei genannten angezeigt werden und sich bei einem Klick auf den jeweiligen Spielernamen ein Popup mit detaillierter Spielerstatistik öffnet.
Für das Erfassen von Runouts könnte es ja einen entsprechenden Button auf dem Scoreboard neben dem "+" geben, angelehnt eine solche Funktion beim Cuescore Scoreboard.
Aber das wären letztendlich nur Spielereien.
Grundsätzlich ist alles da, was man braucht. Vielen Dank dafür!
-
4noxx
- Kombispieler

- Beiträge: 162
- Registriert: 10.12.21 10:47
- Reputation: 47
- Name: JohnDoe
- Wohnort: Bremen +/-
Re: Scoreboard (HTML)
Nette Punkte. Wäre evtl etwas für die PHP Version. Die HTML Version möchte ich
nur noch Bug frei haben.
nur noch Bug frei haben.
Code: Alles auswählen
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pro Billiard Scoreboard</title>
<style>
:root {
--primary-blue: #50a1e4;
--danger: #e74c3c;
--success: #2ecc71;
--bg-gray: #f4f7f9;
--card-bg: #ffffff;
--text-main: #333333;
--text-label: #555555;
--border-color: #ddd;
--modal-overlay: rgba(0,0,0,0.8);
}
body.dark-mode {
--bg-gray: #2c2c2c;
--card-bg: #1e1e1e;
--text-main: #f4f4f4;
--text-label: #bbbbbb;
--border-color: #444;
}
body {
font-family: 'Segoe UI', Roboto, sans-serif;
background-color: var(--card-bg);
margin: 0; padding: 0; color: var(--text-main);
overflow-x: hidden;
}
#app { width: 100%; min-height: 100vh; display: flex; flex-direction: column; }
.nav-tabs {
display: flex;
background: var(--card-bg);
border-bottom: 2px solid var(--border-color);
position: sticky; top: 0; z-index: 100;
}
.tab-link { flex: 1; padding: 15px 5px; text-align: center; cursor: pointer; font-weight: 600; color: #aaa; border-bottom: 4px solid transparent; font-size: 14px; }
.tab-link.active { color: var(--primary-blue); border-bottom-color: var(--primary-blue); background: rgba(80,161,228,0.05); }
.content-section { display: none; padding: 15px; flex: 1; width: 100%; box-sizing: border-box; }
.content-section.active { display: block; }
.stats-table { width: 100%; border-collapse: collapse; background: var(--bg-gray); border-radius: 12px; }
.stats-table th, .stats-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid var(--border-color); }
.stats-table th { background: var(--primary-blue); color: white; font-size: 12px; }
.setup-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-weight: bold; color: var(--text-label); font-size: 14px; }
input, select { width: 100%; padding: 12px; border-radius: 10px; border: 1px solid var(--border-color); background: var(--card-bg); color: var(--text-main); font-size: 16px; }
.sb-layout { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 10px; flex-wrap: wrap; }
.player-container { flex: 1; min-width: 250px; max-width: 500px; display: flex; flex-direction: column; align-items: center; }
.player-card {
width: 100%; background: var(--primary-blue); color: white; padding: 20px 15px; border-radius: 25px;
text-align: center; border: 5px solid transparent; transition: 0.3s; box-sizing: border-box;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.player-card.active-turn { border-color: var(--text-main); transform: scale(1.02); }
.player-card.winner-card { background: var(--success); border-color: gold; }
.score-num { font-size: clamp(50px, 12vw, 90px); font-weight: 800; line-height: 1; margin: 10px 0; }
.stats-line { font-size: 12px; opacity: 0.9; }
.controls-wrapper { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: 20px; padding-bottom: 20px; }
.controls-row { display: flex; justify-content: center; gap: 12px; width: 100%; flex-wrap: wrap; }
.btn-round {
width: 60px; height: 60px; border-radius: 50%; border: 1px solid var(--border-color);
background: var(--card-bg); cursor: pointer; font-size: 22px; color: var(--primary-blue);
display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 5px rgba(0,0,0,0.1);
padding: 0; flex-shrink: 0;
}
.primary-btn { width: 100%; padding: 16px; border: none; border-radius: 12px; font-size: 16px; font-weight: bold; cursor: pointer; }
.primary-btn.blue { background: var(--primary-blue); color: white; }
.primary-btn.green { background: var(--success); color: white; }
.primary-btn.gray { background: #777; color: white; }
.primary-btn.cancel { background: #333; color: #ccc; border: 1px solid #444; }
.modal-overlay { display: none; position: fixed; inset: 0; background: var(--modal-overlay); z-index: 1000; justify-content: center; align-items: center; padding: 20px; }
.modal-box { background: #1e1e1e; color: white; padding: 25px; border-radius: 25px; width: 100%; max-width: 380px; text-align: center; }
@media (orientation: portrait) {
.sb-layout { flex-direction: column; gap: 40px; }
.player-container { width: 100%; }
.adaptive-row { max-width: 280px; }
}
@media (orientation: landscape) {
.sb-layout { flex-direction: row; align-items: center; gap: 20px; }
.player-container { flex: 1; }
.score-num { font-size: clamp(40px, 10vh, 80px); }
.controls-wrapper { flex-direction: row; justify-content: center; flex-wrap: nowrap; }
.adaptive-row { max-width: none; width: auto; }
}
</style>
</head>
<body>
<div id="app">
<div class="nav-tabs">
<div class="tab-link active" id="t-settings" onclick="app.nav('settings')">SPIELER</div>
<div class="tab-link" id="t-setup" onclick="app.nav('setup')">SETUP</div>
<div class="tab-link" id="t-game" onclick="app.nav('game')">SCORE</div>
</div>
<div id="sec-settings" class="content-section active">
<div style="display: flex; gap: 8px; margin-bottom: 15px;">
<input type="text" id="in-name" placeholder="Name..." style="flex:1;">
<button onclick="app.addPlayer()" class="primary-btn blue" style="width: auto; padding: 0 20px;">+</button>
</div>
<div id="player-list-ui"></div>
<button onclick="app.resetStats()" style="margin-top:20px; color: grey; background: none; border: none; text-decoration: underline; font-size: 12px; width: 100%;">Statistiken zurücksetzen</button>
</div>
<div id="sec-setup" class="content-section">
<div class="setup-group"><label>Disziplin</label><select id="set-disc" onchange="app.onDiscChange()">
<option value="14/1">14/1 Endlos</option><option value="8-Ball">8-Ball</option><option value="9-Ball">9-Ball</option><option value="10-Ball">10-Ball</option><option value="Scoreboard">Scoreboard</option>
</select></div>
<div class="setup-group" id="race-container"><label>Race To</label><input type="number" id="set-race" value="100"></div>
<div class="setup-group" id="break-mode-container" style="display:none;"><label>Anstoß</label><select id="set-break"><option value="wechsel">Wechselbreak</option><option value="winner">Winnerbreak</option></select></div>
<div class="setup-group"><label>Spieleranzahl</label><select id="set-pcount" onchange="app.onPlayerCountChange()"></select></div>
<div id="player-select-area"></div>
<button onclick="app.handleStartCheck()" class="primary-btn green" style="margin-top: 15px;">SPIEL STARTEN</button>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
<button onclick="app.toggleDarkMode()" class="primary-btn gray">DESIGN</button>
<button onclick="app.toggleFS()" class="primary-btn gray">VOLLBILD</button>
</div>
</div>
<div id="sec-game" class="content-section">
<div id="game-title" style="text-align:center; font-weight:bold; color:var(--primary-blue); margin-bottom:10px; font-size:14px;"></div>
<div id="sb-container" class="sb-layout"></div>
<div id="winner-area"></div>
<div id="controls-area" class="controls-wrapper"></div>
</div>
<div id="confirm-restart-modal" class="modal-overlay"><div class="modal-box"><h3>Neustart?</h3><button class="primary-btn green" onclick="app.confirmRestart()">JA</button><button class="primary-btn cancel" onclick="app.closeModal('confirm-restart-modal')" style="margin-top:10px;">ABBRECHEN</button></div></div>
<div id="confirm-del-modal" class="modal-overlay"><div class="modal-box"><h3>Spieler löschen?</h3><div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"><button class="primary-btn green" onclick="app.confirmDelPlayer()">JA</button><button class="primary-btn cancel" onclick="app.closeModal('confirm-del-modal')">ABBRUCH</button></div></div></div>
<div id="kugel-popup" class="modal-overlay"><div class="modal-box"><h3>Kugeln am Tisch?</h3><button id="foul-toggle-btn" class="primary-btn" style="background:none; color:var(--primary-blue); border:2px solid var(--primary-blue); margin-bottom:15px;" onclick="app.toggleFoulFrame()">FOUL?</button><div id="kugel-grid" style="display:grid; grid-template-columns: repeat(4, 1fr); gap: 10px;"></div><button onclick="app.closeModal('kugel-popup')" class="primary-btn cancel" style="margin-top:15px;">Abbrechen</button></div></div>
<div id="break-choice-popup" class="modal-overlay"><div class="modal-box"><h3>Wer beginnt?</h3><div id="break-choice-btns"></div></div></div>
</div>
<script>
const app = {
players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Spieler 1", "Spieler 2"],
stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
game: null, history: [], foulActive: false, tempDelIdx: null,
init() {
this.renderPlayerList();
this.onDiscChange();
if(localStorage.getItem('darkMode') === 'true') document.body.classList.add('dark-mode');
},
nav(id) {
document.querySelectorAll('.content-section').forEach(s => s.classList.remove('active'));
document.querySelectorAll('.tab-link').forEach(t => t.classList.remove('active'));
document.getElementById('sec-' + id).classList.add('active');
document.getElementById('t-' + id).classList.add('active');
if(id === 'game') this.renderGame();
},
renderPlayerList() {
const ui = document.getElementById('player-list-ui');
let html = `<table class="stats-table"><thead><tr><th>Name</th><th>S/N</th><th>HS</th><th></th></tr></thead><tbody>`;
this.players.forEach((p, i) => {
const s = this.stats[p] || { w: 0, l: 0, hs: 0 };
html += `<tr><td><strong>${p}</strong></td><td><span style="color:green">${s.w}</span>/<span style="color:red">${s.l}</span></td><td>${s.hs}</td>
<td style="text-align:right;"><button style="background:var(--danger); color:white; border:none; border-radius:5px; padding:5px 10px;" onclick="app.delPlayer(${i})">X</button></td></tr>`;
});
ui.innerHTML = html + `</tbody></table>`;
this.renderPlayerSelects();
},
addPlayer() {
const n = document.getElementById('in-name').value.trim();
if(n && !this.players.includes(n)) { this.players.push(n); this.saveDB(); this.renderPlayerList(); document.getElementById('in-name').value = ""; }
},
delPlayer(i) {
this.tempDelIdx = i;
document.getElementById('confirm-del-modal').style.display = 'flex';
},
confirmDelPlayer() {
if(this.tempDelIdx !== null) {
this.players.splice(this.tempDelIdx, 1);
this.saveDB();
this.renderPlayerList();
this.closeModal('confirm-del-modal');
this.tempDelIdx = null;
}
},
resetStats() { if(confirm("Statistiken aller Spieler löschen?")) { this.stats = {}; this.saveDB(); this.renderPlayerList(); } },
saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); localStorage.setItem('billiardStats', JSON.stringify(this.stats)); },
onDiscChange() {
const d = document.getElementById('set-disc').value;
const races = { "14/1": 100, "8-Ball": 5, "9-Ball": 6, "10-Ball": 7, "Scoreboard": 0 };
document.getElementById('set-race').value = races[d];
document.getElementById('race-container').style.display = d === 'Scoreboard' ? 'none' : 'block';
const pSelect = document.getElementById('set-pcount');
pSelect.innerHTML = "";
const max = (d === 'Scoreboard') ? 6 : 2;
for(let i=1; i<=max; i++) pSelect.innerHTML += `<option value="${i}" ${i===2?'selected':''}>${i} Spieler</option>`;
this.onPlayerCountChange();
},
onPlayerCountChange() {
const pCount = parseInt(document.getElementById('set-pcount').value);
const disc = document.getElementById('set-disc').value;
document.getElementById('break-mode-container').style.display = (["8-Ball", "9-Ball", "10-Ball"].includes(disc) && pCount > 1) ? 'block' : 'none';
this.renderPlayerSelects();
},
renderPlayerSelects() {
const area = document.getElementById('player-select-area');
const count = document.getElementById('set-pcount').value;
area.innerHTML = "";
for(let i=0; i<count; i++) {
let def = this.players[i] || this.players[0] || "";
area.innerHTML += `<div class="setup-group"><label>Spieler ${i+1}</label><select class="sel-player">${this.players.map(p => `<option value="${p}" ${p===def?'selected':''}>${p}</option>`).join('')}</select></div>`;
}
},
handleStartCheck() {
const disc = document.getElementById('set-disc').value;
if (["8-Ball", "9-Ball", "10-Ball"].includes(disc)) this.openBreakChoicePopup();
else this.startGame();
},
startGame(starterIdx = 0) {
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
this.game = {
disc: document.getElementById('set-disc').value, names, scores: names.map(() => 0),
activeIdx: starterIdx, balls: 15, race: parseInt(document.getElementById('set-race').value) || 0,
breakMode: document.getElementById('set-break').value,
stats: names.map(() => ({ innings: 1, currentSeries: 0, hs: 0 })), over: false, recorded: false
};
this.history = []; this.renderGame(); this.nav('game');
},
renderGame() {
const g = this.game; if(!g) return;
const is141 = g.disc === "14/1";
const isSB = g.disc === "Scoreboard";
const container = document.getElementById('sb-container');
const controls = document.getElementById('controls-area');
container.innerHTML = ""; controls.innerHTML = "";
let winnerIdx = (!isSB) ? g.scores.findIndex(s => s >= g.race) : -1;
g.over = winnerIdx !== -1;
if(g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }
g.names.forEach((name, i) => {
const isTurn = g.activeIdx === i && g.names.length > 1;
container.innerHTML += `
<div class="player-container">
<div class="player-card ${isTurn?'active-turn':''} ${i===winnerIdx?'winner-card':''}">
<div style="font-weight:bold; font-size:16px;">${name}</div>
<div class="score-num">${g.scores[i]}</div>
${is141 ? `<div class="stats-line">HS: ${g.stats[i].hs} | Aufn: ${g.stats[i].innings}</div>` : ''}
</div>
${(!is141) ? `<div style="display:flex; gap:15px; margin-top:10px;">
<button class="btn-round" onclick="app.addScore(${i}, -1)">-</button>
<button class="btn-round" onclick="app.addScore(${i}, 1)">+</button>
</div>` : ''}
</div>`;
if(is141 && i === 0 && g.names.length === 2) {
container.innerHTML += `<div class="switch-container"><button class="btn-round" onclick="app.switchPlayer()">⇄</button></div>`;
}
});
if(is141) {
controls.innerHTML = `
<div class="controls-row adaptive-row">
<button class="btn-round" onclick="app.requestRestart()">↺</button>
<button class="btn-round" onclick="app.undo()">↶</button>
<button class="btn-round" onclick="app.change141(-1)">-1</button>
<button class="btn-round" onclick="app.change141(1)">+1</button>
<button class="btn-round" onclick="app.rack141()">△</button>
<button class="btn-round" style="background:var(--primary-blue); color:white; font-size:16px; font-weight:bold;" onclick="app.openKugelPopup()">${g.balls}</button>
<button class="btn-round" style="color:red;" onclick="app.foul141()">⚡</button>
</div>
`;
} else {
controls.innerHTML = `
<div class="controls-row">
<button class="btn-round" onclick="app.requestRestart()">↺</button>
<button class="btn-round" onclick="app.undo()">↶</button>
</div>
`;
}
if(g.over) document.getElementById('winner-area').innerHTML = `<button class="primary-btn green" onclick="app.handleRematch()">REMATCH</button>`;
else document.getElementById('winner-area').innerHTML = "";
document.getElementById('game-title').innerText = isSB ? "SCOREBOARD" : `${g.disc} (RACE TO ${g.race})`;
},
addScore(idx, v) { this.saveStep(); this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v); this.renderGame(); },
change141(v) {
const p = this.game.activeIdx; this.saveStep();
if(v > 0) {
this.game.scores[p]++; this.game.stats[p].currentSeries++;
if(this.game.balls <= 1) this.game.balls = 15; else this.game.balls--;
} else {
if(this.game.balls < 15) { this.game.scores[p]--; this.game.balls++; this.game.stats[p].currentSeries--; }
}
if(this.game.stats[p].currentSeries > this.game.stats[p].hs) this.game.stats[p].hs = this.game.stats[p].currentSeries;
this.renderGame();
},
rack141() { this.saveStep(); const p = this.game.activeIdx; this.game.scores[p] += (this.game.balls - 1); this.game.balls = 15; this.renderGame(); },
switchPlayer() { this.saveStep(); this.game.stats[this.game.activeIdx].currentSeries = 0; this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length; this.game.stats[this.game.activeIdx].innings++; this.renderGame(); },
foul141() { this.saveStep(); this.game.scores[this.game.activeIdx]--; this.switchPlayer(); },
openKugelPopup() {
this.foulActive = false; const grid = document.getElementById('kugel-grid'); grid.innerHTML = "";
for(let i=1; i<=15; i++) {
grid.innerHTML += `<button class="btn-round" style="width:100%" onclick="app.setRest(${i})">${i}</button>`;
}
document.getElementById('kugel-popup').style.display = 'flex';
},
setRest(n) {
this.saveStep(); const p = this.game.activeIdx;
this.game.scores[p] += (this.game.balls - n);
this.game.balls = n; this.closeModal('kugel-popup'); this.switchPlayer();
},
toggleFoulFrame() { this.foulActive = !this.foulActive; document.getElementById('foul-toggle-btn').style.color = this.foulActive ? 'red' : 'var(--primary-blue)'; },
requestRestart() { document.getElementById('confirm-restart-modal').style.display = 'flex'; },
confirmRestart() { this.closeModal('confirm-restart-modal'); this.startGame(this.game.activeIdx); },
closeModal(id) { document.getElementById(id).style.display = 'none'; },
handleRematch() { this.startGame(); },
openBreakChoicePopup() {
const area = document.getElementById('break-choice-btns');
const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
area.innerHTML = names.map((n, i) => `<button class="primary-btn green" style="margin-bottom:8px;" onclick="app.closeBreakPopup(${i})">${n}</button>`).join('');
document.getElementById('break-choice-popup').style.display = 'flex';
},
closeBreakPopup(idx) { document.getElementById('break-choice-popup').style.display = 'none'; this.startGame(idx); },
saveStep() { this.history.push(JSON.stringify(this.game)); },
undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } },
recordResult(wIdx) {
this.game.names.forEach((n, i) => {
if(!this.stats[n]) this.stats[n] = { w: 0, l: 0, hs: 0 };
if(i === wIdx) this.stats[n].w++; else this.stats[n].l++;
if(this.game.stats[i].hs > this.stats[n].hs) this.stats[n].hs = this.game.stats[i].hs;
});
this.saveDB(); this.renderPlayerList();
},
toggleDarkMode() { const d = document.body.classList.toggle('dark-mode'); localStorage.setItem('darkMode', d); },
toggleFS() { if(!document.fullscreenElement) document.documentElement.requestFullscreen(); else document.exitFullscreen(); }
};
app.init();
</script>
</body>
</html>- Dateianhänge
-
- Scoreboard_01.png (37.87 KiB) 786 mal betrachtet