Scoreboard (HTML)

Alles was nicht unbedingt mit Billard zu tun hat

Moderator: Moderatoren

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

Scoreboard (HTML)

Beitrag von 4noxx »

Ich spiele gerade etwas rum mit einem weiteren Scoreboard. Läuft auf einem lokalen Webserver (nginx).
Vielleicht kann sowas einer gebrauchen oder auch gerne erweitern.
(Beta, Fehler möglich)

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;
            --bg-gray: #f4f7f9;
        }

        body { font-family: 'Segoe UI', sans-serif; background-color: #e9ecef; margin: 0; padding: 20px; display: flex; justify-content: center; }
        #app { width: 100%; max-width: 900px; background: white; border-radius: 25px; overflow: hidden; box-shadow: 0 15px 40px rgba(0,0,0,0.1); min-height: 800px; position: relative; }
        
        /* NAVBAR */
        .nav-tabs { display: flex; background: #fff; border-bottom: 2px solid #dce0e5; }
        .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: #f8fbff; }
        
        .content-section { display: none; padding: 30px; }
        .content-section.active { display: block; }

        /* TAB 1: SPIELER LISTE */
        .player-db-entry { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 8px; }
        .del-btn { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 5px 10px; cursor: pointer; color: #666; }

        /* SETUP & INPUTS */
        .setup-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
        input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid #ddd; font-size: 16px; box-sizing: border-box; background: #fff; }

        /* SCOREBOARD */
        .sb-layout { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 20px; }
        .sb-grid-free { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }

        .player-card { background: var(--primary-blue); color: #fff; padding: 25px; border-radius: 25px; text-align: center; border: 6px solid transparent; transition: 0.3s; flex: 1; box-shadow: 0 8px 20px rgba(0,0,0,0.1); }
        .player-card.active-turn { border-color: #444; transform: scale(1.02); }
        .score-num { font-size: 110px; font-weight: 800; margin: 10px 0; line-height: 0.9; }
        .stats-row { font-size: 14px; opacity: 0.9; margin-top: 10px; }

        /* BUTTONS */
        .controls-row { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 40px; }
        .btn-icon { width: 75px; height: 75px; border-radius: 50%; border: 3px solid var(--primary-blue); background: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
        .btn-icon svg { width: 35px; height: 35px; fill: var(--primary-blue); }
        .btn-icon.balls-display { background: var(--primary-blue); color: white; border: none; font-size: 32px; font-weight: bold; }
        .btn-swap { background: white; border: 1px solid #ddd; border-radius: 10px; width: 60px; height: 60px; cursor: pointer; font-size: 28px; display: flex; align-items: center; justify-content: center; }

        .primary-btn { width: 100%; padding: 18px; background: var(--primary-blue); color: white; border: none; border-radius: 15px; font-size: 20px; font-weight: bold; cursor: pointer; margin-top: 20px; }

        /* POPUP */
        .modal-overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; justify-content: center; align-items: center; border-radius: 25px; }
        .modal-box { background: white; padding: 25px; border-radius: 25px; width: 340px; text-align: center; }
        
        .foul-toggle { width: 100%; padding: 15px; background: var(--primary-blue); color: white; border: 3px solid transparent; border-radius: 12px; font-weight: bold; margin-bottom: 15px; cursor: pointer; font-size: 18px; text-transform: uppercase; }
        .foul-toggle.active { border-color: #000; box-shadow: 0 0 0 2px #000; }
        
        .kugel-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
        .k-btn { padding: 12px; background: var(--primary-blue); border: none; color: white; border-radius: 10px; font-weight: bold; cursor: pointer; font-size: 16px; }
        .k-btn:disabled { background: #e0e0e0; color: #aaa; cursor: not-allowed; }
    </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>
    </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="141">14/1 Endlos</option>
                <option value="8ball">8-Ball</option>
                <option value="9ball">9-Ball</option>
                <option value="10ball">10-Ball</option>
                <option value="scoreboard">Freies 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">
            <label>Anzahl Spieler:</label>
            <select id="set-pcount" onchange="app.renderPlayerSelects()"></select>
        </div>
        <div id="player-select-area"></div>
        <button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
    </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:18px;"></div>
        <div id="sb-container"></div>

        <div id="main-controls" class="controls-row">
            <button class="btn-icon" onclick="app.changeScore(-1)"><span style="font-size:40px; color:var(--primary-blue);">-</span></button>
            <button class="btn-icon" onclick="app.changeScore(1)"><span style="font-size:40px; color:var(--primary-blue);">+</span></button>
            
            <button class="btn-icon" id="btn-rack" onclick="app.rackFinish()">
                <svg viewBox="0 0 24 24"><path d="M12,2L2,22H22L12,2M12,6L19.53,20H4.47L12,6Z"/></svg>
            </button>
            
            <button class="btn-icon balls-display" id="balls-display" onclick="app.openKugelPopup()">15</button>
            
            <button class="btn-icon" id="btn-foul" onclick="app.handleBlitzFoul()">
                <svg viewBox="0 0 24 24"><path d="M7,2V13H10V22L17,10H13L17,2H7Z"/></svg>
            </button>
        </div>

        <div style="text-align:center; margin-top:40px;">
            <button onclick="app.undo()" style="padding:12px 25px; border-radius:12px; border:1px solid #ccc; background:white; cursor:pointer;">Schritt Rückgängig</button>
        </div>
    </div>

    <div id="kugel-popup" class="modal-overlay">
        <div class="modal-box">
            <button id="foul-toggle-btn" class="foul-toggle" onclick="app.toggleFoulFrame()">FOUL</button>
            <div class="kugel-grid" id="kugel-grid"></div>
            <button onclick="app.closeKugelPopup()" style="width:100%; margin-top:20px; border:none; background:none; color:#999; cursor:pointer;">Abbrechen</button>
        </div>
    </div>
</div>

<script>
const app = {
    players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Bert", "Grobi", "Ernie", "Graf Zahl"],
    game: null, history: [], foulActive: false,

    init() { this.renderPlayerList(); this.onDiscChange(); },

    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');
    },

    renderPlayerList() {
        const ui = document.getElementById('player-list-ui');
        ui.innerHTML = this.players.map((p, i) => `
            <div class="player-db-entry">
                <span>${p}</span>
                <button class="del-btn" onclick="app.delPlayer(${i})">x</button>
            </div>
        `).join('');
    },

    addPlayer() {
        const n = document.getElementById('in-name').value.trim();
        if(n) { this.players.push(n); this.saveDB(); this.renderPlayerList(); this.renderPlayerSelects(); document.getElementById('in-name').value=""; }
    },

    delPlayer(i) { this.players.splice(i,1); this.saveDB(); this.renderPlayerList(); this.renderPlayerSelects(); },
    saveDB() { localStorage.setItem('billiardPlayers', JSON.stringify(this.players)); },

    onDiscChange() {
        const d = document.getElementById('set-disc').value;
        const races = { "141": 100, "8ball": 5, "9ball": 6, "10ball": 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.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++) {
            // Logik für unterschiedliche Vorbelegung
            let defaultValue = "";
            if(this.players.length > i) {
                defaultValue = this.players[i];
            } else if (this.players.length > 0) {
                defaultValue = this.players[0];
            }

            area.innerHTML += `
                <div class="setup-group">
                    <label>Spieler ${i+1}:</label>
                    <select class="sel-player" onchange="app.validateUniquePlayers(this)">
                        ${this.players.map(p => `<option value="${p}" ${p === defaultValue ? 'selected' : ''}>${p}</option>`).join('')}
                    </select>
                </div>`;
        }
        this.validateUniquePlayers();
    },

    validateUniquePlayers() {
        const selects = document.querySelectorAll('.sel-player');
        const picked = Array.from(selects).map(s => s.value);
        selects.forEach(s => {
            const currentVal = s.value;
            Array.from(s.options).forEach(opt => {
                opt.disabled = picked.includes(opt.value) && opt.value !== currentVal;
            });
        });
    },

    startGame() {
        const disc = document.getElementById('set-disc').value;
        const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
        this.game = {
            disc, names, scores: Array(names.length).fill(0), 
            activeIdx: 0, balls: 15, race: document.getElementById('set-race').value,
            stats: names.map(() => ({ innings: 1, series: 0, max: 0 }))
        };
        this.renderGame();
        this.nav('game');
    },

    renderGame() {
        const g = this.game;
        document.getElementById('game-title').innerText = `${g.disc.toUpperCase()} (Race to ${g.race})`;
        const container = document.getElementById('sb-container');
        container.innerHTML = "";

        if(g.disc === 'scoreboard') {
            container.className = "sb-grid-free";
            g.names.forEach((name, i) => {
                container.innerHTML += `
                    <div class="player-card">
                        <div style="font-size:18px;">${name}</div>
                        <div class="score-num" style="font-size:70px;">${g.scores[i]}</div>
                        <div style="display:flex; justify-content:center; gap:10px;">
                            <button class="btn-swap" style="width:40px; height:40px; font-size:20px;" onclick="app.changeFreeScore(${i},-1)">-</button>
                            <button class="btn-swap" style="width:40px; height:40px; font-size:20px;" onclick="app.changeFreeScore(${i},1)">+</button>
                        </div>
                    </div>`;
            });
        } else {
            container.className = "sb-layout";
            g.names.forEach((name, i) => {
                const gd = g.stats[i].innings > 0 ? (g.scores[i] / g.stats[i].innings).toFixed(2) : "0.00";
                const cardHtml = `
                    <div class="player-card ${g.activeIdx === i ? 'active-turn' : ''}">
                        <div style="font-size:24px; font-weight:bold;">${name}</div>
                        <div class="score-num">${g.scores[i]}</div>
                        ${g.disc === '141' ? `
                            <div class="stats-row">Aufnahme: ${g.stats[i].innings} | GD: ${gd}</div>
                            <div class="stats-row">Serie: ${g.stats[i].series} | Max: ${g.stats[i].max}</div>
                        ` : ''}
                    </div>`;
                
                if(i === 1) container.innerHTML += `<button class="btn-swap" onclick="app.switchPlayer()">⇄</button>`;
                container.innerHTML += cardHtml;
            });
        }

        const is141 = g.disc === '141';
        document.getElementById('main-controls').style.display = g.disc === 'scoreboard' ? 'none' : 'flex';
        document.getElementById('btn-rack').style.display = is141 ? 'flex' : 'none';
        
        document.getElementById('btn-foul').style.display = is141 ? 'flex' : 'none';
        
        document.getElementById('balls-display').style.display = is141 ? 'flex' : 'none';
        document.getElementById('balls-display').innerText = g.balls;
    },

    switchPlayer() {
        this.saveStep();
        const p = this.game.activeIdx;
        this.game.stats[p].series = 0;
        this.game.activeIdx = (p + 1) % 2;
        this.game.stats[this.game.activeIdx].innings++;
        this.renderGame();
    },

    changeScore(v) {
        this.saveStep();
        const p = this.game.activeIdx;
        this.game.scores[p] += v;
        if(this.game.disc === '141' && v > 0) {
            this.game.stats[p].series += v;
            if(this.game.stats[p].series > this.game.stats[p].max) this.game.stats[p].max = this.game.stats[p].series;
            this.game.balls = this.game.balls > 1 ? this.game.balls - 1 : 15;
        }
        this.renderGame();
    },

    changeFreeScore(i, v) { this.saveStep(); this.game.scores[i] += v; this.renderGame(); },
    rackFinish() { this.saveStep(); this.game.scores[this.game.activeIdx] += (this.game.balls - 1); this.game.balls = 15; this.renderGame(); },
    handleBlitzFoul() { this.saveStep(); this.game.scores[this.game.activeIdx] -= 1; this.switchPlayer(); },

    openKugelPopup() {
        this.foulActive = false;
        const foulBtn = document.getElementById('foul-toggle-btn');
        foulBtn.classList.remove('active');
        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.onclick = () => {
                this.saveStep();
                const diff = this.game.balls - i;
                const points = diff - (this.foulActive ? 1 : 0);
                this.game.scores[this.game.activeIdx] += points;
                if(points > 0 && this.game.disc === '141') {
                    this.game.stats[this.game.activeIdx].series += points;
                    if(this.game.stats[this.game.activeIdx].series > this.game.stats[this.game.activeIdx].max) 
                        this.game.stats[this.game.activeIdx].max = this.game.stats[this.game.activeIdx].series;
                }
                this.game.balls = i <= 1 ? 15 : i;
                this.closeKugelPopup(); this.switchPlayer();
            };
            grid.appendChild(b);
        }
        document.getElementById('kugel-popup').style.display = 'flex';
    },

    toggleFoulFrame() { this.foulActive = !this.foulActive; document.getElementById('foul-toggle-btn').classList.toggle('active', this.foulActive); },
    closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; },
    saveStep() { this.history.push(JSON.stringify(this.game)); },
    undo() { if(this.history.length) { this.game = JSON.parse(this.history.pop()); this.renderGame(); } }
};

app.init();
</script>
</body>
</html>
Dateianhänge
Scoreboard_05.png
Scoreboard_05.png (62.5 KiB) 1092 mal betrachtet
Scoreboard_04.png
Scoreboard_04.png (50 KiB) 1092 mal betrachtet
Scoreboard_03.png
Scoreboard_03.png (68.27 KiB) 1092 mal betrachtet
Scoreboard_02.png
Scoreboard_02.png (44.3 KiB) 1092 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (33.4 KiB) 1092 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Leider kann man hier nicht dauerhaft den Beitrag editieren.... :wei:
Für jede kleine Änderung einen neuen Post ist nervig.

Evtl packe ich den Code woanders hin....

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;
        }

        body { font-family: 'Segoe UI', sans-serif; background-color: #e9ecef; margin: 0; padding: 20px; display: flex; justify-content: center; }
        #app { width: 100%; max-width: 900px; background: white; border-radius: 25px; overflow: hidden; box-shadow: 0 15px 40px rgba(0,0,0,0.1); min-height: 850px; position: relative; }
        
        /* NAVIGATION */
        .nav-tabs { display: flex; background: #fff; border-bottom: 2px solid #dce0e5; }
        .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: #f8fbff; }
        
        .content-section { display: none; padding: 30px; }
        .content-section.active { display: block; }

        /* SPIELER LISTE */
        .player-db-entry { display: flex; flex-direction: column; padding: 15px 20px; background: var(--bg-gray); border-radius: 12px; margin-bottom: 10px; border: 1px solid #eee; }
        .p-header { display: flex; justify-content: space-between; align-items: center; }
        .p-stats { font-size: 13px; color: #777; margin-top: 5px; font-weight: 600; }
        .del-btn { background: #fff; border: 1px solid #ddd; border-radius: 6px; padding: 5px 10px; cursor: pointer; color: #666; }

        /* SETUP */
        .setup-group { margin-bottom: 20px; }
        label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
        input, select { width: 100%; padding: 14px; border-radius: 12px; border: 1px solid #ddd; font-size: 16px; box-sizing: border-box; background: #fff; }

        /* SCOREBOARD */
        .sb-layout { display: flex; align-items: center; justify-content: center; gap: 15px; margin-top: 20px; }
        .sb-grid-free { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }

        .player-card { background: var(--primary-blue); color: #fff; padding: 30px 20px; border-radius: 35px; text-align: center; border: 6px solid transparent; transition: 0.3s; flex: 1; box-shadow: 0 10px 25px rgba(80,161,228,0.2); }
        .player-card.active-turn { border-color: #333; transform: scale(1.02); }
        .player-card.winner-card { background: var(--success); border-color: gold; }
        
        .score-num { font-size: 110px; font-weight: 800; margin: 10px 0; line-height: 0.9; }
        .stats-row { font-size: 14px; opacity: 0.9; margin-top: 5px; }

        /* UNIFORM BUTTONS (90x90) */
        .controls-row { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 15px; margin-top: 40px; }
        .btn-round { 
            width: 90px; height: 90px; 
            border-radius: 50%; 
            border: 2px solid #ddd; 
            background: white; 
            display: flex; align-items: center; justify-content: center; 
            cursor: pointer; 
            box-shadow: 0 4px 10px rgba(0,0,0,0.05);
            transition: transform 0.1s, border-color 0.2s;
            font-size: 40px;
            color: var(--primary-blue);
            padding: 0;
            flex-shrink: 0;
        }
        .btn-round:active { transform: translateY(3px); }
        .btn-round:disabled { opacity: 0.3; filter: grayscale(1); cursor: not-allowed; }
        .btn-round svg { width: 35px; height: 35px; fill: #999; }
        
        .btn-round.primary-circle { border-color: var(--primary-blue); }
        .btn-round.fill { background: var(--primary-blue); color: white; border: none; font-weight: bold; font-size: 36px; }

        .btn-swap-small { background: white; border: 1px solid #ddd; border-radius: 10px; width: 50px; height: 50px; cursor: pointer; font-size: 24px; display: flex; align-items: center; justify-content: center; }

        .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%; margin-top: 20px; }

        /* MODAL */
        .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: white; padding: 30px; border-radius: 30px; width: 360px; text-align: center; }
        .foul-btn-large { width: 100%; padding: 15px; background: #f8f9fa; border: 2px solid #ddd; border-radius: 15px; font-weight: bold; margin-bottom: 20px; cursor: pointer; font-size: 18px; }
        .foul-btn-large.active { background: var(--danger); color: white; border-color: var(--danger); }
        .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; font-size: 18px; }
    </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">
            <label>Anzahl Spieler:</label>
            <select id="set-pcount" onchange="app.renderPlayerSelects()"></select>
        </div>
        <div id="player-select-area"></div>
        <button onclick="app.startGame()" class="primary-btn">SPIEL STARTEN</button>
    </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 class="controls-row">
            <button class="btn-round" onclick="app.undo()" title="Schritt rückgängig">
                <svg viewBox="0 0 24 24"><path d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z"/></svg>
            </button>

            <button class="btn-round primary-circle control-input" onclick="app.changeScore(-1)">–</button>
            <button class="btn-round primary-circle control-input" onclick="app.changeScore(1)">+</button>
            
            <button class="btn-round primary-circle control-input" id="btn-rack" onclick="app.rackFinish()">
                <svg viewBox="0 0 24 24" style="fill:var(--primary-blue)"><path d="M12,2L2,22H22L12,2M12,6L19.53,20H4.47L12,6Z"/></svg>
            </button>
            
            <button class="btn-round fill control-input" id="balls-display" onclick="app.openKugelPopup()">15</button>
            
            <button class="btn-round primary-circle control-input" id="btn-foul" onclick="app.handleBlitzFoul()">
                <svg viewBox="0 0 24 24" style="fill:var(--primary-blue)"><path d="M7,2V13H10V22L17,10H13L17,2H7Z"/></svg>
            </button>
        </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" class="foul-btn-large" onclick="app.toggleFoulFrame()">⚠️ FOUL? (Hier tippen)</button>
            <div class="kugel-grid" id="kugel-grid"></div>
            <button onclick="app.closeKugelPopup()" style="width:100%; margin-top:25px; border:none; background:none; color:#999; cursor:pointer;">Abbrechen</button>
        </div>
    </div>
</div>

<script>
const app = {
    players: JSON.parse(localStorage.getItem('billiardPlayers')) || ["Bert", "Grobi", "Ernie", "Graf Zahl"],
    stats: JSON.parse(localStorage.getItem('billiardStats')) || {},
    game: null, history: [], foulActive: false,

    init() { this.renderPlayerList(); this.onDiscChange(); },
    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');
    },

    renderPlayerList() {
        const ui = document.getElementById('player-list-ui');
        ui.innerHTML = this.players.map((p, i) => {
            const s = this.stats[p] || { w: 0, l: 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: ${s.w} Siege / ${s.l} Niederlagen</div></div>`;
        }).join('');
    },

    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(); },
    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.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" onchange="app.validateUniquePlayers()">${this.players.map(p => `<option value="${p}" ${p === defVal ? 'selected' : ''}>${p}</option>`).join('')}</select></div>`;
        }
        this.validateUniquePlayers();
    },

    validateUniquePlayers() {
        const selects = document.querySelectorAll('.sel-player');
        const picked = Array.from(selects).map(s => s.value);
        selects.forEach(s => { Array.from(s.options).forEach(opt => opt.disabled = picked.includes(opt.value) && opt.value !== s.value); });
    },

    startGame() {
        const disc = document.getElementById('set-disc').value;
        const names = Array.from(document.querySelectorAll('.sel-player')).map(s => s.value);
        this.game = {
            disc, names, scores: Array(names.length).fill(0), 
            activeIdx: 0, balls: 15, race: parseInt(document.getElementById('set-race').value),
            stats: names.map(() => ({ innings: 1, series: 0, max: 0 })), over: false, recorded: false
        };
        this.history = [];
        this.renderGame();
        this.nav('game');
    },

    renderGame() {
        const g = this.game;
        document.getElementById('game-title').innerText = `${g.disc} (Race to ${g.race})`;
        const container = document.getElementById('sb-container');
        const winnerArea = document.getElementById('winner-area');
        container.innerHTML = ""; winnerArea.innerHTML = "";

        let winnerIdx = g.disc !== 'Scoreboard' ? g.scores.findIndex(s => s >= g.race) : -1;
        g.over = winnerIdx !== -1;

        if (g.over && !g.recorded) { this.recordResult(winnerIdx); g.recorded = true; }

        if(g.disc === 'Scoreboard') {
            container.className = "sb-grid-free";
            g.names.forEach((name, i) => {
                container.innerHTML += `<div class="player-card"><div style="font-size:20px; font-weight:bold;">${name}</div><div class="score-num" style="font-size:70px;">${g.scores[i]}</div><div style="display:flex; justify-content:center; gap:10px;"><button class="btn-swap-small" onclick="app.changeFreeScore(${i},-1)">-</button><button class="btn-swap-small" onclick="app.changeFreeScore(${i},1)">+</button></div></div>`;
            });
        } else {
            container.className = "sb-layout";
            g.names.forEach((name, i) => {
                const isW = i === winnerIdx;
                const gd = g.stats[i].innings > 0 ? (g.scores[i] / g.stats[i].innings).toFixed(2) : "0.00";
                container.innerHTML += `<div class="player-card ${g.activeIdx === i ? 'active-turn' : ''} ${isW ? 'winner-card' : ''}"><div style="font-size:24px; font-weight:bold;">${isW ? '🏆 ' + name : name}</div><div class="score-num">${g.scores[i]}</div>${g.disc === '14/1' ? `<div class="stats-row">Aufnahme: ${g.stats[i].innings} | GD: ${gd}</div><div class="stats-row">Serie: ${g.stats[i].series} | Max: ${g.stats[i].max}</div>` : ''}</div>`;
                if(i === 0 && g.names.length === 2) container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over ? 'disabled' : ''}>⇄</button>`;
            });
            if(g.over) winnerArea.innerHTML = `<button class="restart-btn" onclick="app.startGame()">REMATCH STARTEN</button>`;
        }

        const is141 = g.disc === '14/1';
        document.querySelectorAll('.control-input').forEach(btn => btn.disabled = g.over);
        
        // Anzeige je nach Disziplin
        document.getElementById('btn-rack').style.display = is141 ? 'flex' : 'none';
        document.getElementById('btn-foul').style.display = is141 ? 'flex' : 'none';
        document.getElementById('balls-display').style.display = is141 ? 'flex' : 'none';
        
        if(is141) document.getElementById('balls-display').innerText = g.balls;
    },

    recordResult(winnerIdx) {
        this.game.names.forEach((name, i) => {
            if(!this.stats[name]) this.stats[name] = { w: 0, l: 0 };
            if(i === winnerIdx) this.stats[name].w++; else this.stats[name].l++;
        });
        this.saveDB(); this.renderPlayerList();
    },

    resetStats() { if(confirm("Statistiken löschen?")) { this.stats = {}; this.saveDB(); this.renderPlayerList(); } },
    switchPlayer() { if(this.game.over) return; this.saveStep(); this.game.activeIdx = (this.game.activeIdx + 1) % this.game.names.length; if(this.game.disc !== 'Scoreboard') { this.game.stats[this.game.activeIdx].innings++; this.game.stats[this.game.activeIdx].series = 0; } this.renderGame(); },
    changeScore(v) { if(this.game.over) return; this.saveStep(); const p = this.game.activeIdx; this.game.scores[p] += v; if(this.game.disc === '14/1') { if(v > 0) { this.game.stats[p].series += v; if(this.game.stats[p].series > this.game.stats[p].max) this.game.stats[p].max = this.game.stats[p].series; this.game.balls = this.game.balls > 1 ? this.game.balls - 1 : 15; } else { this.game.balls = this.game.balls < 15 ? this.game.balls + 1 : 15; } } this.renderGame(); },
    changeFreeScore(i, v) { this.saveStep(); this.game.scores[i] += v; this.renderGame(); },
    rackFinish() { if(this.game.over) return; this.saveStep(); const pts = (this.game.balls - 1); this.game.scores[this.game.activeIdx] += pts; this.game.stats[this.game.activeIdx].series += pts; if(this.game.stats[this.game.activeIdx].series > this.game.stats[this.game.activeIdx].max) this.game.stats[this.game.activeIdx].max = this.game.stats[this.game.activeIdx].series; this.game.balls = 15; this.renderGame(); },
    handleBlitzFoul() {
    if (this.game.over) return;
    this.saveStep();

    const pIdx = this.game.activeIdx;
    const isPlayerOne = (pIdx === 0);
    const isFirstInning = (this.game.stats[pIdx].innings === 1);
    
    // Sonderregel: Spieler 1, erste Aufnahme, Foul beim Eröffnungsstoß
    // Wir prüfen auf >= 0, um sicherzustellen, dass es der erste Abzug ist
    if (this.game.disc === '14/1' && isPlayerOne && isFirstInning && this.game.scores[pIdx] >= 0) {
        this.game.scores[pIdx] -= 2;
    } else {
        this.game.scores[pIdx] -= 1;
    }

    this.switchPlayer();
},
    openKugelPopup() {
        if(this.game.over) return; this.foulActive = false; document.getElementById('foul-toggle-btn').classList.remove('active');
        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.onclick = () => { this.saveStep(); const diff = this.game.balls - i; const pts = diff - (this.foulActive ? 1 : 0); this.game.scores[this.game.activeIdx] += pts; if(pts > 0) { this.game.stats[this.game.activeIdx].series += pts; if(this.game.stats[this.game.activeIdx].series > this.game.stats[this.game.activeIdx].max) this.game.stats[this.game.activeIdx].max = this.game.stats[this.game.activeIdx].series; } this.game.balls = i <= 1 ? 15 : i; this.closeKugelPopup(); this.switchPlayer(); };
            grid.appendChild(b);
        }
        document.getElementById('kugel-popup').style.display = 'flex';
    },
    toggleFoulFrame() { this.foulActive = !this.foulActive; document.getElementById('foul-toggle-btn').classList.toggle('active', this.foulActive); },
    closeKugelPopup() { document.getElementById('kugel-popup').style.display = 'none'; },
    saveStep() { this.history.push(JSON.stringify(this.game)); },
    undo() { 
        if(this.history.length) { 
            const prev = JSON.parse(this.history.pop());
            if(this.game.recorded && !prev.recorded) {
                const winnerIdx = this.game.scores.findIndex(s => s >= this.game.race);
                this.game.names.forEach((name, i) => { if(i === winnerIdx) this.stats[name].w--; else this.stats[name].l--; });
                this.saveDB(); this.renderPlayerList();
            }
            this.game = prev; this.renderGame(); 
        } 
    }
};
app.init();
</script>
</body>
</html>
Dateianhänge
Scoreboard_02.png
Scoreboard_02.png (88.48 KiB) 1074 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (52.76 KiB) 1074 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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); }

        /* Scoreboard Layouts */
        .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; }
    </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">
            <label>Anzahl Spieler:</label>
            <select id="set-pcount" onchange="app.renderPlayerSelects()"></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>

<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');
    },

    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');
    },

    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.players.push(n); this.saveDB(); this.renderPlayerList(); document.getElementById('in-name').value=""; }
    },

    delPlayer(i) { this.players.splice(i,1); 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.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);
        this.game = {
            disc, names, scores: Array(names.length).fill(0), 
            activeIdx: 0, balls: 15, race: parseInt(document.getElementById('set-race').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; }

        // Layout Logik
        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>`;

            const card = `
                <div class="player-wrapper">
                    <div class="player-card ${(!isBallGame && !isFreeSB && g.activeIdx === i) ? '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;
            
            // 14/1 Swap Button nur zwischen zwei Spielern
            if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
                container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
            }
        });

        // Bottom Controls
        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 {
            // 14/1 Endlos Controls
            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.startGame()">REMATCH STARTEN</button>`;
    },

    changeScoreDir(idx, v) {
        this.saveStep();
        this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
        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 grid = document.getElementById('kugel-grid'); grid.innerHTML = "";
        document.getElementById('foul-toggle-btn').style.borderColor = "#50a1e4";
        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'; },
    toggleFoulFrame() {
        this.foulActive = !this.foulActive;
        const btn = document.getElementById('foul-toggle-btn');
        btn.style.color = 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();
    },
    resetStats() { if(confirm("Alle Statistiken löschen?")) { this.stats = {}; this.saveDB(); this.renderPlayerList(); } }
};
app.init();
</script>
</body>
</html>
Dateianhänge
Scoreboard_05.png
Scoreboard_05.png (38.65 KiB) 1047 mal betrachtet
Scoreboard_04.png
Scoreboard_04.png (40.52 KiB) 1047 mal betrachtet
Scoreboard_03.png
Scoreboard_03.png (55.18 KiB) 1047 mal betrachtet
Scoreboard_02.png
Scoreboard_02.png (36.04 KiB) 1047 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (34.96 KiB) 1047 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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); }

        /* Scoreboard Layouts */
        .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; }
    </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">
            <label>Anzahl Spieler:</label>
            <select id="set-pcount" onchange="app.renderPlayerSelects()"></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>

<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');
    },

    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');
    },

    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.players.push(n); this.saveDB(); this.renderPlayerList(); document.getElementById('in-name').value=""; }
    },

    delPlayer(i) { this.players.splice(i,1); 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.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);
        this.game = {
            disc, names, scores: Array(names.length).fill(0), 
            activeIdx: 0, balls: 15, race: parseInt(document.getElementById('set-race').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; }

        // Layout Logik
        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>`;

            const card = `
                <div class="player-wrapper">
                    <div class="player-card ${(!isBallGame && !isFreeSB && g.activeIdx === i) ? '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;
            
            // 14/1 Swap Button nur zwischen zwei Spielern
            if(!isBallGame && !isFreeSB && i === 0 && g.names.length === 2) {
                container.innerHTML += `<button class="btn-swap-small" onclick="app.switchPlayer()" ${g.over?'disabled':''}>⇄</button>`;
            }
        });

        // Bottom Controls
        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 {
            // 14/1 Endlos Controls
            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.startGame()">REMATCH STARTEN</button>`;
    },

    changeScoreDir(idx, v) {
        this.saveStep();
        this.game.scores[idx] = Math.max(0, this.game.scores[idx] + v);
        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";       // Standard-Blau
			foulBtn.style.borderColor = "#50a1e4"; // Standard-Blau
		}
        const grid = document.getElementById('kugel-grid'); grid.innerHTML = "";
        document.getElementById('foul-toggle-btn').style.borderColor = "#50a1e4";
        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";
    },

    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();
    },
    resetStats() { if(confirm("Alle Statistiken löschen?")) { this.stats = {}; 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 »

Sieht echt klasse aus und lässt sich sehr einfach bedienen!
Eine Auswahl, ob Winner- oder Wechselbreak mit einem kleinen Indikator im Scoreboard wäre noch super.
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

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); 
        }
        
        /* Der Rahmen für den aktiven Anstoß */
        .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; }
    </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.renderPlayerSelects()"></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>

<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');
    },

    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');
    },

    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.players.push(n); this.saveDB(); this.renderPlayerList(); document.getElementById('in-name').value=""; }
    },

    delPlayer(i) { this.players.splice(i,1); 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 isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(d);
        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';
        
        // Break-Modus nur bei Ball-Spielen anzeigen
        document.getElementById('break-mode-container').style.display = isBallGame ? 'block' : 'none';

        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.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, // Speichern ob Winner- oder Wechselbreak
            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; }

        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>`;

            // active-turn steuert den schwarzen Rahmen für den Anstoß
            const card = `
                <div class="player-wrapper">
                    <div class="player-card ${((isBallGame || !isFreeSB) && g.activeIdx === i) ? '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.startGame()">REMATCH STARTEN</button>`;
    },

    changeScoreDir(idx, v) {
        this.saveStep();
        const g = this.game;
        g.scores[idx] = Math.max(0, g.scores[idx] + v);
        
        // Logik für Anstoßwechsel bei 8, 9, 10-Ball
        const isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(g.disc);
        if (isBallGame && v === 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();
    },
    resetStats() { if(confirm("Alle Statistiken löschen?")) { this.stats = {}; this.saveDB(); this.renderPlayerList(); } }
};
app.init();
</script>
</body>
</html>
Dateianhänge
Scoreboard_02.png
Scoreboard_02.png (47.78 KiB) 1006 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (39.45 KiB) 1006 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Gleicher Kram nochmal, aber mit PIN Code Abfrage gegen "versehentliches" löschen :-)
PIN: 12345 (steht in Zeile 207)

Code: Alles auswählen

    // Sicherheitseinstellungen
    correctPin: "12345"

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 Styles */
        #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.renderPlayerSelects()"></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="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,
    
    // Sicherheitseinstellungen
    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');
    },

    // PIN Logik
    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 isBallGame = ["8-Ball", "9-Ball", "10-Ball"].includes(d);
        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';
        
        document.getElementById('break-mode-container').style.display = isBallGame ? 'block' : 'none';

        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.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';
        
        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>`;

            const card = `
                <div class="player-wrapper">
                    <div class="player-card ${((isBallGame || !isFreeSB) && g.activeIdx === i) ? '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.startGame()">REMATCH STARTEN</button>`;
    },

    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) {
            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 (44.42 KiB) 1004 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Aktueller Stand (PHP-Version)

Spielernamen werden auf dem Webserver gespeichert, falls man mehrere Tisch nutzt
Man kann einen Tisch auswählen.
Es gibt eine Übersichtsseite für die Spielstände (Live)
Dateianhänge
Scoreboard_02.png
Scoreboard_02.png (28.35 KiB) 986 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (49.99 KiB) 986 mal betrachtet
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 »

Wäre noch cool, wenn nach jedem "SPIEL STARTEN", "REMATCH" und "Partie neustarten" immer eine Abfrage käme, wer das Anstoßrecht hat.
Denn gerade wenn man mehrere Sätze mit nem Kumpel spielt, wechselt der Anstoß gerne ab.
Aktuell müsste man im Setup nach jedem Satz die Spielernamen vertauschen.

Bei Spieler Anzahl "1" hingegen müssten sinngemäß Anstoßmodus und Indikator komplett ausgeblendet werden.
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

ok
Dateianhänge
Scoreboard_01.png
Scoreboard_01.png (51.54 KiB) 956 mal betrachtet
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Hier mal die PHP Files.

- Anpassung URL
http://192.168.1.80/?Tisch=1
(Tisch 1 ist voreingestellt)
Bei http://192.168.1.80/?Tisch=0 ist die Tischauswahl deaktiviert.
Bei http://192.168.1.82 kann Tisch ausgewählt werden

- Abfrage beim 8-Ball / 9-Ball / 10-Ball hinzugefügt, wer das erste Break hat (Rematch)

- Bei 8-Ball / 9-Ball / 10-Ball keine Auswahl vom Anstoss, wenn Spieler = 1

- Installationshilfe in help.html (oder hier :zuf:) --- http://192.168.1.80/help.html

Alpha / Beta Version oder was auch immer. Fehler möglich.

Copyright: Keins, Anpassungen / Ideen erwünscht :zwi:
Dateianhänge
Scoreboard_02.png
Scoreboard_02.png (163.24 KiB) 951 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (32.24 KiB) 951 mal betrachtet
Scoreboard.zip
(12.96 KiB) 36-mal heruntergeladen
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 »

4noxx hat geschrieben: 14.01.26 13:34ok
Mega! Dankeschön!
Postest du noch den neuen HTML-Code dazu?
4noxx
Kombispieler
Kombispieler
Beiträge: 160
Registriert: 10.12.21 10:47
Reputation: 47
Name: JohnDoe
Wohnort: Bremen +/-

Re: Scoreboard (HTML)

Beitrag von 4noxx »

HTML

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;
        }

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

Re: Scoreboard (HTML)

Beitrag von 4noxx »

Habs etwas für mein Tablet optimiert ...

Frame entfernt, damit der Bildschirm besser gefüllt ist...

Ich ändere nun nur noch die php version.
Dateianhänge
Scoreboard_v06.zip
(13.82 KiB) 35-mal heruntergeladen
Scoreboard_02.png
Scoreboard_02.png (79.76 KiB) 920 mal betrachtet
Scoreboard_01.png
Scoreboard_01.png (34.62 KiB) 920 mal betrachtet
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 »

Die Anstoß-Abfrage bleibt bei mir mit dem neuen Code bisher leider aus.
Antworten

Zurück zu „Off Topic“