Rengarace / templates /index.html
Renday's picture
Update templates/index.html
4aef391 verified
<!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));
// ADMIN UPDATE: Erkennt Logouts und Ready-Status sofort
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);
// Gestreifte Ziellinie
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>