Scoreboard (HTML)

Alles was nicht unbedingt mit Billard zu tun hat

Moderator: Moderatoren

4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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.
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Bei mir geht... komisch.
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
Scoreboard_01.png (45.52 KiB) 968 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

und hier nochmal ohne "Container"....

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>
Benutzeravatar
AIMhAK
Pomeranzenkiller
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)

Beitrag von AIMhAK »

Habe es am PC mit Edge, Chrome und Firefox getestet und am Smartphone mit Chrome und Samsung Webbrowser...
Das Abfragefenster erscheint leider nicht.
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

schon komisch... bei mir gehts

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
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

oder diesen

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>
Benutzeravatar
AIMhAK
Pomeranzenkiller
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)

Beitrag von AIMhAK »

Mit beiden Codes wird auch unter Verwendung des Fully Kiosk Browsers kein Popup zur Abfrage des Anstoßrechts geöffnet. :wei:
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
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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:

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>
Benutzeravatar
AIMhAK
Pomeranzenkiller
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)

Beitrag von AIMhAK »

Mit dem letzten Code funktioniert es.
Vielen Dank! :ban:
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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>
Benutzeravatar
AIMhAK
Pomeranzenkiller
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)

Beitrag von AIMhAK »

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:

Bild
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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.
Benutzeravatar
AIMhAK
Pomeranzenkiller
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)

Beitrag von AIMhAK »

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!
4noxx
Kombispieler
Kombispieler
Beiträge: 162
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Nette Punkte. Wäre evtl etwas für die PHP Version. Die HTML Version möchte ich
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
Scoreboard_01.png (37.87 KiB) 786 mal betrachtet
Antworten

Zurück zu „Off Topic“