| <!DOCTYPE html> |
| <html lang="de"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> |
| <title>GamerJam | Pferderennen 1920</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script> |
| <style> |
| * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } |
| html, body { margin: 0; padding: 0; width: 100%; height: 100dvh; background-color: #050a05; overflow: hidden; font-family: sans-serif; } |
| @keyframes gallop { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } |
| .animate-gallop { animation: gallop 0.6s infinite; } |
| .btn-effect { box-shadow: 0 4px 15px rgba(229, 62, 62, 0.5); transition: all 0.2s; } |
| .btn-training { border: 2px solid #FFC72C; color: #FFC72C; transition: all 0.2s; } |
| .btn-training:hover { background: #FFC72C; color: black; } |
| #gameWrapper { display: none; flex-direction: column; height: 100dvh; width: 100%; } |
| #canvasContainer { flex: 1; display: flex; align-items: center; justify-content: center; position: relative; background: #0a0a0a url('assets/fpeople.png') repeat; background-size: cover; overflow: hidden; } |
| canvas { display: block; background: #1a471a; border: 2px solid #000; z-index: 5; box-shadow: 0 0 20px rgba(0,0,0,0.5); } |
| #ui { background: #000; padding: 12px; border-top: 3px solid #32CD32; z-index: 20; text-align: center; flex-shrink: 0; } |
| .bet-circle { display: inline-block; width: 42px; height: 42px; border-radius: 50%; cursor: pointer; border: 3px solid transparent; transition: 0.2s; margin: 2px; } |
| .selected { border-color: #FFC72C !important; transform: scale(1.1); box-shadow: 0 0 15px #FFC72C; position: relative; } |
| .selected::after { content: '✔'; position: absolute; top: -8px; right: -4px; background: #FFC72C; color: black; border-radius: 50%; width: 18px; height: 18px; font-size: 10px; font-weight: bold; line-height: 18px; } |
| #overlay { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.98); padding: 20px; border: 3px solid #FFC72C; z-index: 100; text-align: center; border-radius: 20px; width: 80%; max-width: 280px; } |
| #chatBox { position: absolute; bottom: 10px; left: 10px; width: 180px; height: 120px; background: rgba(0,0,0,0.8); border: 1px solid #FFC72C; border-radius: 8px; z-index: 60; display: flex; flex-direction: column; } |
| #chatMessages { flex-grow: 1; overflow-y: auto; padding: 5px; font-size: 10px; text-align: left; } |
| #chatInput { background: #111; color: white; border: none; border-top: 1px solid #333; padding: 5px; font-size: 11px; outline: none; width: 100%; border-radius: 0 0 8px 8px; } |
| .type-btn { background: #222; border: 1px solid #444; padding: 6px 12px; border-radius: 5px; font-size: 11px; font-weight: bold; cursor: pointer; color: white; } |
| .type-btn.active { background: #FFC72C; color: black; border-color: #fff; } |
| #rankingTable { position: absolute; top: 15px; left: 15px; background: rgba(0,0,0,0.85); padding: 8px; border-radius: 8px; border: 2px solid #FFC72C; z-index: 50; min-width: 120px; text-align: left; } |
| </style> |
| </head> |
| <body class="text-white"> |
|
|
| <audio id="soundStart" src="assets/start.mp3"></audio> |
| <audio id="soundRace" src="assets/trap.mp3" loop></audio> |
| <audio id="soundHumans" src="assets/humans.mp3" loop></audio> |
| <audio id="soundWinner" src="assets/winner.mp3"></audio> |
|
|
| <div id="loginOverlay" class="fixed inset-0 flex items-center justify-center p-4 bg-[#050a05] z-[200]"> |
| <div class="w-full max-w-xl bg-black rounded-xl p-8 border-4 border-yellow-500/70 text-center"> |
| <div class="inline-block mb-4 animate-gallop"><img src="assets/ping.png" alt="Logo" width="80"></div> |
| <h1 class="text-3xl md:text-5xl font-extrabold text-yellow-500 mb-4 uppercase">Pferderennen 1920</h1> |
| <div class="space-y-4"> |
| <input type="email" id="emailInput" placeholder="E-Mail Adresse" class="w-full p-3 rounded bg-gray-900 border border-yellow-500 text-white text-center focus:outline-none"> |
| <div class="flex flex-col gap-3"> |
| <button onclick="doLogin()" class="w-full bg-red-600 btn-effect text-white text-xl font-bold py-4 rounded-full uppercase">Jetzt Live Wetten</button> |
| <a href="https://www.gamerjam.de/game/horse/renrace.html" class="w-full btn-training text-lg font-bold py-3 rounded-full uppercase flex items-center justify-center">Übungsrennen (Training)</a> |
| </div> |
| </div> |
| <p class="mt-6 text-gray-500 text-xs italic">GamerJam - Die goldene Ära des Turf</p> |
| </div> |
| </div> |
|
|
| <div id="gameWrapper"> |
| <div class="p-2 flex justify-between items-center bg-black border-b border-green-900"> |
| <span class="text-yellow-500 font-bold text-[10px] uppercase">GamerJam Racing</span> |
| <div id="historyDots" class="flex gap-1"></div> |
| <button onclick="location.reload()" class="bg-red-900 text-[9px] px-2 py-1 rounded">Logout</button> |
| </div> |
|
|
| <div id="canvasContainer"> |
| <div id="rankingTable"><div id="rankingList" class="text-[10px]"></div></div> |
| <div id="chatBox"><div id="chatMessages"></div><input type="text" id="chatInput" placeholder="Chat..." onkeypress="handleChat(event)"></div> |
| <canvas id="gameCanvas" width="1200" height="700"></canvas> |
| <div id="overlay"> |
| <h2 id="modalTitle" class="text-xl font-bold text-yellow-500 mb-2 uppercase">ERGEBNIS</h2> |
| <img id="winnerLargeImg" src="" class="w-16 h-16 mx-auto mb-2 object-contain"> |
| <p id="modalMessage" class="text-sm mb-4 font-bold"></p> |
| <button onclick="setReady()" class="bg-yellow-500 text-black px-6 py-2 rounded-lg font-bold w-full uppercase">Bereit</button> |
| </div> |
| </div> |
|
|
| <div id="ui"> |
| <div class="flex justify-center gap-2 mb-3"> |
| <button onclick="setBetType('win')" id="btn-win" class="type-btn active">SIEG (x6)</button> |
| <button onclick="setBetType('place')" id="btn-place" class="type-btn">PLATZ (x2.5)</button> |
| <button onclick="setBetType('quartet')" id="btn-quartet" class="type-btn">TOP 4 (x1.8)</button> |
| </div> |
| <div class="flex justify-center items-center gap-6 mb-3"> |
| <div class="text-xl font-bold text-green-500" id="bankDisplay">1000$</div> |
| <div id="adminArea" class="hidden"><button id="startRace" onclick="requestStart()" disabled class="bg-red-600 px-4 py-1 rounded font-bold text-xs uppercase disabled:opacity-30">Warten...</button></div> |
| <div class="flex items-center gap-2"> |
| <button onclick="changeBet(-10)" class="bg-gray-800 w-8 h-8 rounded-full font-bold">-</button> |
| <span id="betAmount" class="text-lg font-bold">50$</span> |
| <button onclick="changeBet(10)" class="bg-gray-800 w-8 h-8 rounded-full font-bold">+</button> |
| </div> |
| </div> |
| <div id="betOptions" class="flex justify-center flex-wrap gap-1"></div> |
| </div> |
| </div> |
|
|
| <script> |
| const socket = io(); |
| const canvas = document.getElementById('gameCanvas'); |
| const ctx = canvas.getContext('2d'); |
| const COLOR_DATA = [{name:"YellowSun",rgb:"#FFD700",file:"pferd_gelb.png"},{name:"GreenLeaf",rgb:"#32CD32",file:"pferd_gruen.png"},{name:"SilverBullet",rgb:"#C0C0C0",file:"pferd_silber.png"},{name:"RedRocket",rgb:"#DC143C",file:"pferd_rot.png"},{name:"OrangeFlame",rgb:"#FF8C00",file:"pferd_orange.png"},{name:"BlueNote",rgb:"#1E90FF",file:"pferd_blau.png"}]; |
| const MULTIPLIERS = { win: 6, place: 2.5, quartet: 1.8 }; |
| let money = 1000, betAmount = 50, betOn = null, betType = 'win', racing = false, userEmail = "", isAdmin = false, raceHistory = []; |
| let finishOrder = []; |
| |
| function resizeCanvas() { |
| const container = document.getElementById('canvasContainer'); |
| if (!container) return; |
| const ratio = 1200 / 700; |
| let w = container.clientWidth - 20, h = w / ratio; |
| if (h > container.clientHeight - 20) { h = container.clientHeight - 20; w = h * ratio; } |
| canvas.style.width = w + "px"; canvas.style.height = h + "px"; |
| } |
| window.addEventListener('resize', resizeCanvas); |
| |
| async function doLogin() { |
| const email = document.getElementById('emailInput').value.trim(); |
| if(!email) return; |
| const res = await fetch('/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email }) }); |
| const data = await res.json(); |
| userEmail = data.email; money = data.money; isAdmin = data.is_admin; |
| document.getElementById('loginOverlay').style.display = 'none'; |
| document.getElementById('gameWrapper').style.display = 'flex'; |
| setTimeout(resizeCanvas, 100); |
| if(isAdmin) document.getElementById('adminArea').classList.remove('hidden'); |
| renderBetOptions(); updateUI(); |
| socket.emit('join_race', { email: userEmail }); |
| } |
| |
| function handleChat(e) { if (e.key === 'Enter') { const input = document.getElementById('chatInput'); if (input.value.trim()) { socket.emit('chat_message', { email: userEmail, message: input.value.trim() }); input.value = ''; } } } |
| socket.on('new_chat_message', (data) => { |
| const chat = document.getElementById('chatMessages'); |
| chat.innerHTML += `<div><span class="text-yellow-500 font-bold">${data.email.split('@')[0]}:</span> ${data.message}</div>`; |
| chat.scrollTop = chat.scrollHeight; |
| }); |
| |
| class Horse { |
| constructor(index, data) { this.index = index; this.color = data.rgb; this.name = data.name; this.relX = 0.08; this.img = new Image(); this.img.src = "assets/" + data.file; this.speedMult = 1; this.finished = false; } |
| draw() { |
| const x = this.relX * 1200, y = (0.2 + this.index * 0.12) * 700; |
| if(betOn === this.index) { ctx.fillStyle = "#FFC72C"; ctx.beginPath(); ctx.moveTo(x, y-65); ctx.lineTo(x-10, y-80); ctx.lineTo(x+10, y-80); ctx.fill(); } |
| const bounce = (racing && !this.finished) ? Math.sin(Date.now()/110)*2.5 : 0; |
| if(this.img.complete) ctx.drawImage(this.img, x-50, y-50+bounce, 100, 100); |
| } |
| } |
| const horses = COLOR_DATA.map((d, i) => new Horse(i, d)); |
| |
| |
| socket.on('ready_update', (data) => { |
| if(isAdmin) { |
| const btn = document.getElementById('startRace'); |
| btn.disabled = (data.ready === 0); |
| btn.innerText = `START (${data.ready}/${data.total} Live)`; |
| btn.style.backgroundColor = (data.ready > 0) ? "#dc2626" : "#444"; |
| } |
| }); |
| |
| socket.on('race_started', (data) => { |
| racing = true; finishOrder = []; |
| if(betOn !== null) money -= betAmount; |
| updateUI(); |
| horses.forEach((h, i) => { h.relX = 0.08; h.finished = false; h.speedMult = data.speeds[i]; }); |
| document.getElementById('soundStart').play().catch(()=>{}); |
| document.getElementById('soundRace').play().catch(()=>{}); |
| document.getElementById('soundHumans').play().catch(()=>{}); |
| }); |
| |
| function gameLoop() { |
| ctx.clearRect(0,0,1200,700); |
| |
| const fX = 1050, fW = 40; |
| for(let r=0; r<14; r++) { for(let c=0; c<2; c++) { ctx.fillStyle = (r+c)%2===0 ? "#fff":"#000"; ctx.fillRect(fX+c*(fW/2), r*50, fW/2, 50); } } |
| |
| horses.forEach((h, i) => { |
| const y = (0.2 + i * 0.12) * 700; |
| ctx.fillStyle = h.color + "1A"; ctx.fillRect(0, y-40, 1200, 80); |
| if(racing && !h.finished) { |
| h.relX += 0.0022 * h.speedMult; |
| if(h.relX >= 0.865) { h.relX = 0.865; h.finished = true; finishOrder.push(h.index); if (finishOrder.length === horses.length) setTimeout(finishRace, 500); } |
| } |
| h.draw(); |
| }); |
| if(racing) updateRanking(); |
| requestAnimationFrame(gameLoop); |
| } |
| |
| function updateRanking() { |
| let displayList = []; |
| finishOrder.forEach(idx => displayList.push(horses[idx])); |
| let stillRacing = horses.filter(h => !h.finished).sort((a,b) => b.relX - a.relX); |
| displayList = displayList.concat(stillRacing); |
| document.getElementById('rankingList').innerHTML = displayList.map((h, i) => ` |
| <div class="flex justify-between gap-2"><span>${i+1}. ${h.name}</span><span class="${h.finished ? 'text-yellow-500' : 'opacity-50'}">${h.finished ? 'ZIEL' : Math.round(h.relX*100)+'%'}</span></div> |
| `).join(''); |
| } |
| |
| function finishRace() { |
| if(!racing) return; racing = false; |
| const winner = horses[finishOrder[0]]; |
| raceHistory.unshift(winner.color); if(raceHistory.length > 5) raceHistory.pop(); |
| document.getElementById('historyDots').innerHTML = raceHistory.map(c => `<div class="w-2 h-2 rounded-full border border-white" style="background:${c}"></div>`).join(''); |
| document.getElementById('soundRace').pause(); document.getElementById('soundHumans').pause(); document.getElementById('soundWinner').play().catch(()=>{}); |
| let won = (betType === 'win' && betOn === finishOrder[0]) || (betType === 'place' && finishOrder.slice(0, 3).includes(betOn)) || (betType === 'quartet' && finishOrder.slice(0, 4).includes(betOn)); |
| if(won) money += Math.floor(betAmount * MULTIPLIERS[betType]); |
| document.getElementById('winnerLargeImg').src = winner.img.src; |
| document.getElementById('overlay').style.display = 'block'; |
| document.getElementById('modalTitle').innerText = won ? "SIEG!" : "ENDE"; |
| document.getElementById('modalMessage').innerText = winner.name + " gewinnt!"; |
| updateUI(); socket.emit('update_money', { email: userEmail, money: money }); |
| } |
| |
| function renderBetOptions() { document.getElementById('betOptions').innerHTML = COLOR_DATA.map((d, i) => `<div class="bet-circle" style="background: ${d.rgb}" onclick="selectHorse(${i})" id="horse-${i}"></div>`).join(''); } |
| function selectHorse(index) { if(racing || document.getElementById('overlay').style.display === 'block') return; betOn = index; document.querySelectorAll('.bet-circle').forEach(el => el.classList.remove('selected')); document.getElementById(`horse-${index}`).classList.add('selected'); socket.emit('player_ready', { email: userEmail }); } |
| function changeBet(val) { if(!racing) { betAmount = Math.max(10, Math.min(money, betAmount + val)); updateUI(); } } |
| function updateUI() { document.getElementById('bankDisplay').innerText = money + "$"; document.getElementById('betAmount').innerText = betAmount + "$"; } |
| function setBetType(type) { if(!racing) { betType = type; document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active')); document.getElementById('btn-' + type).classList.add('active'); } } |
| function requestStart() { if(isAdmin && !racing) socket.emit('start_race', { email: userEmail }); } |
| function setReady() { document.getElementById('overlay').style.display = 'none'; betOn = null; renderBetOptions(); finishOrder = []; } |
| |
| gameLoop(); |
| </script> |
| </body> |
| </html> |