Update templates/index.html
Browse files- templates/index.html +40 -159
templates/index.html
CHANGED
|
@@ -8,72 +8,25 @@
|
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
| 9 |
<style>
|
| 10 |
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
| 11 |
-
|
| 12 |
-
html, body {
|
| 13 |
-
margin: 0; padding: 0; width: 100%; height: 100dvh;
|
| 14 |
-
background-color: #050a05; overflow: hidden; font-family: sans-serif;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
@keyframes gallop { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
| 18 |
.animate-gallop { animation: gallop 0.6s infinite; }
|
| 19 |
.btn-effect { box-shadow: 0 4px 15px rgba(229, 62, 62, 0.5); transition: all 0.2s; }
|
| 20 |
.btn-training { border: 2px solid #FFC72C; color: #FFC72C; transition: all 0.2s; }
|
| 21 |
.btn-training:hover { background: #FFC72C; color: black; }
|
| 22 |
-
|
| 23 |
-
#
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
height: 100dvh;
|
| 27 |
-
width: 100%;
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
#canvasContainer {
|
| 31 |
-
flex: 1;
|
| 32 |
-
display: flex;
|
| 33 |
-
align-items: center;
|
| 34 |
-
justify-content: center;
|
| 35 |
-
position: relative;
|
| 36 |
-
background: #0a0a0a url('assets/fpeople.png') repeat;
|
| 37 |
-
background-size: cover;
|
| 38 |
-
overflow: hidden;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
canvas {
|
| 42 |
-
display: block;
|
| 43 |
-
background: #1a471a;
|
| 44 |
-
border: 2px solid #000;
|
| 45 |
-
z-index: 5;
|
| 46 |
-
box-shadow: 0 0 20px rgba(0,0,0,0.5);
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
#ui {
|
| 50 |
-
background: #000;
|
| 51 |
-
padding: 12px;
|
| 52 |
-
border-top: 3px solid #32CD32;
|
| 53 |
-
z-index: 20;
|
| 54 |
-
text-align: center;
|
| 55 |
-
flex-shrink: 0;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
.bet-circle { display: inline-block; width: 42px; height: 42px; border-radius: 50%; cursor: pointer; border: 3px solid transparent; transition: 0.2s; margin: 2px; }
|
| 59 |
.selected { border-color: #FFC72C !important; transform: scale(1.1); box-shadow: 0 0 15px #FFC72C; position: relative; }
|
| 60 |
.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; }
|
| 61 |
-
|
| 62 |
#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; }
|
| 63 |
-
|
| 64 |
#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; }
|
| 65 |
#chatMessages { flex-grow: 1; overflow-y: auto; padding: 5px; font-size: 10px; text-align: left; }
|
| 66 |
#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; }
|
| 67 |
-
|
| 68 |
.type-btn { background: #222; border: 1px solid #444; padding: 6px 12px; border-radius: 5px; font-size: 11px; font-weight: bold; cursor: pointer; color: white; }
|
| 69 |
.type-btn.active { background: #FFC72C; color: black; border-color: #fff; }
|
| 70 |
-
|
| 71 |
-
@media (orientation: portrait) {
|
| 72 |
-
#chatBox { display: none; }
|
| 73 |
-
#rankingTable { scale: 0.8; top: 5px; left: 5px; }
|
| 74 |
-
.type-btn { padding: 4px 8px; font-size: 9px; }
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
#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; }
|
| 78 |
</style>
|
| 79 |
</head>
|
|
@@ -81,25 +34,18 @@
|
|
| 81 |
|
| 82 |
<audio id="soundStart" src="assets/start.mp3"></audio>
|
| 83 |
<audio id="soundRace" src="assets/trap.mp3" loop></audio>
|
| 84 |
-
<audio id="soundHumans" src="assets/humans.
|
| 85 |
<audio id="soundWinner" src="assets/winner.mp3"></audio>
|
| 86 |
|
| 87 |
<div id="loginOverlay" class="fixed inset-0 flex items-center justify-center p-4 bg-[#050a05] z-[200]">
|
| 88 |
<div class="w-full max-w-xl bg-black rounded-xl p-8 border-4 border-yellow-500/70 text-center">
|
| 89 |
<div class="inline-block mb-4 animate-gallop"><img src="assets/ping.png" alt="Logo" width="80"></div>
|
| 90 |
<h1 class="text-3xl md:text-5xl font-extrabold text-yellow-500 mb-4 uppercase">Pferderennen 1920</h1>
|
| 91 |
-
|
| 92 |
<div class="space-y-4">
|
| 93 |
<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">
|
| 94 |
-
|
| 95 |
<div class="flex flex-col gap-3">
|
| 96 |
-
<button onclick="doLogin()" class="w-full bg-red-600 btn-effect text-white text-xl font-bold py-4 rounded-full uppercase">
|
| 97 |
-
|
| 98 |
-
</button>
|
| 99 |
-
|
| 100 |
-
<a href="https://www.gamerjam.de/game/rrg/renrace.html" class="w-full btn-training text-lg font-bold py-3 rounded-full uppercase flex items-center justify-center">
|
| 101 |
-
Übungsrennen (Training)
|
| 102 |
-
</a>
|
| 103 |
</div>
|
| 104 |
</div>
|
| 105 |
<p class="mt-6 text-gray-500 text-xs italic">GamerJam - Die goldene Ära des Turf</p>
|
|
@@ -114,17 +60,9 @@
|
|
| 114 |
</div>
|
| 115 |
|
| 116 |
<div id="canvasContainer">
|
| 117 |
-
<div id="rankingTable">
|
| 118 |
-
|
| 119 |
-
</div>
|
| 120 |
-
|
| 121 |
-
<div id="chatBox">
|
| 122 |
-
<div id="chatMessages"></div>
|
| 123 |
-
<input type="text" id="chatInput" placeholder="Chat..." onkeypress="handleChat(event)">
|
| 124 |
-
</div>
|
| 125 |
-
|
| 126 |
<canvas id="gameCanvas" width="1200" height="700"></canvas>
|
| 127 |
-
|
| 128 |
<div id="overlay">
|
| 129 |
<h2 id="modalTitle" class="text-xl font-bold text-yellow-500 mb-2 uppercase">ERGEBNIS</h2>
|
| 130 |
<img id="winnerLargeImg" src="" class="w-16 h-16 mx-auto mb-2 object-contain">
|
|
@@ -139,12 +77,9 @@
|
|
| 139 |
<button onclick="setBetType('place')" id="btn-place" class="type-btn">PLATZ (x2.5)</button>
|
| 140 |
<button onclick="setBetType('quartet')" id="btn-quartet" class="type-btn">TOP 4 (x1.8)</button>
|
| 141 |
</div>
|
| 142 |
-
|
| 143 |
<div class="flex justify-center items-center gap-6 mb-3">
|
| 144 |
<div class="text-xl font-bold text-green-500" id="bankDisplay">1000$</div>
|
| 145 |
-
<div id="adminArea" class="hidden">
|
| 146 |
-
<button id="startRace" onclick="requestStart()" disabled class="bg-red-600 px-4 py-1 rounded font-bold text-xs uppercase disabled:opacity-30">START</button>
|
| 147 |
-
</div>
|
| 148 |
<div class="flex items-center gap-2">
|
| 149 |
<button onclick="changeBet(-10)" class="bg-gray-800 w-8 h-8 rounded-full font-bold">-</button>
|
| 150 |
<span id="betAmount" class="text-lg font-bold">50$</span>
|
|
@@ -159,16 +94,7 @@
|
|
| 159 |
const socket = io();
|
| 160 |
const canvas = document.getElementById('gameCanvas');
|
| 161 |
const ctx = canvas.getContext('2d');
|
| 162 |
-
|
| 163 |
-
const COLOR_DATA = [
|
| 164 |
-
{name: "YellowSun", rgb: "#FFD700", file: "pferd_gelb.png"},
|
| 165 |
-
{name: "GreenLeaf", rgb: "#32CD32", file: "pferd_gruen.png"},
|
| 166 |
-
{name: "SilverBullet", rgb: "#C0C0C0", file: "pferd_silber.png"},
|
| 167 |
-
{name: "RedRocket", rgb: "#DC143C", file: "pferd_rot.png"},
|
| 168 |
-
{name: "OrangeFlame", rgb: "#FF8C00", file: "pferd_orange.png"},
|
| 169 |
-
{name: "BlueNote", rgb: "#1E90FF", file: "pferd_blau.png"}
|
| 170 |
-
];
|
| 171 |
-
|
| 172 |
const MULTIPLIERS = { win: 6, place: 2.5, quartet: 1.8 };
|
| 173 |
let money = 1000, betAmount = 50, betOn = null, betType = 'win', racing = false, userEmail = "", isAdmin = false, raceHistory = [];
|
| 174 |
let finishOrder = [];
|
|
@@ -177,8 +103,7 @@
|
|
| 177 |
const container = document.getElementById('canvasContainer');
|
| 178 |
if (!container) return;
|
| 179 |
const ratio = 1200 / 700;
|
| 180 |
-
let w = container.clientWidth - 20;
|
| 181 |
-
let h = w / ratio;
|
| 182 |
if (h > container.clientHeight - 20) { h = container.clientHeight - 20; w = h * ratio; }
|
| 183 |
canvas.style.width = w + "px"; canvas.style.height = h + "px";
|
| 184 |
}
|
|
@@ -198,35 +123,34 @@
|
|
| 198 |
socket.emit('join_race', { email: userEmail });
|
| 199 |
}
|
| 200 |
|
| 201 |
-
function handleChat(e) {
|
| 202 |
-
if (e.key === 'Enter') {
|
| 203 |
-
const input = document.getElementById('chatInput');
|
| 204 |
-
if (input.value.trim()) { socket.emit('chat_message', { email: userEmail, message: input.value.trim() }); input.value = ''; }
|
| 205 |
-
}
|
| 206 |
-
}
|
| 207 |
socket.on('new_chat_message', (data) => {
|
| 208 |
const chat = document.getElementById('chatMessages');
|
| 209 |
-
|
| 210 |
-
chat.innerHTML += `<div><span class="text-yellow-500 font-bold">${name}:</span> ${data.message}</div>`;
|
| 211 |
chat.scrollTop = chat.scrollHeight;
|
| 212 |
});
|
| 213 |
|
| 214 |
class Horse {
|
| 215 |
-
constructor(index, data) {
|
| 216 |
-
this.index = index; this.color = data.rgb; this.name = data.name; this.relX = 0.08;
|
| 217 |
-
this.img = new Image(); this.img.src = "assets/" + data.file; this.speedMult = 1; this.finished = false;
|
| 218 |
-
}
|
| 219 |
draw() {
|
| 220 |
const x = this.relX * 1200, y = (0.2 + this.index * 0.12) * 700;
|
| 221 |
-
if(betOn === this.index) {
|
| 222 |
-
ctx.fillStyle = "#FFC72C"; ctx.beginPath(); ctx.moveTo(x, y-65); ctx.lineTo(x-10, y-80); ctx.lineTo(x+10, y-80); ctx.fill();
|
| 223 |
-
}
|
| 224 |
const bounce = (racing && !this.finished) ? Math.sin(Date.now()/110)*2.5 : 0;
|
| 225 |
if(this.img.complete) ctx.drawImage(this.img, x-50, y-50+bounce, 100, 100);
|
| 226 |
}
|
| 227 |
}
|
| 228 |
const horses = COLOR_DATA.map((d, i) => new Horse(i, d));
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
socket.on('race_started', (data) => {
|
| 231 |
racing = true; finishOrder = [];
|
| 232 |
if(betOn !== null) money -= betAmount;
|
|
@@ -239,99 +163,56 @@
|
|
| 239 |
|
| 240 |
function gameLoop() {
|
| 241 |
ctx.clearRect(0,0,1200,700);
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
for(let row=0; row<14; row++){
|
| 246 |
-
for(let col=0; col<2; col++){
|
| 247 |
-
ctx.fillStyle = (row + col) % 2 === 0 ? "#fff" : "#000";
|
| 248 |
-
ctx.fillRect(fX + col*(fW/2), row*50, fW/2, 50);
|
| 249 |
-
}
|
| 250 |
-
}
|
| 251 |
|
| 252 |
horses.forEach((h, i) => {
|
| 253 |
const y = (0.2 + i * 0.12) * 700;
|
| 254 |
ctx.fillStyle = h.color + "1A"; ctx.fillRect(0, y-40, 1200, 80);
|
| 255 |
-
if(racing && !h.finished) {
|
| 256 |
-
h.relX += 0.0022 * h.speedMult;
|
| 257 |
-
if(h.relX >= 0.865) {
|
| 258 |
-
h.relX = 0.865; h.finished = true; finishOrder.push(h.index);
|
| 259 |
-
if (finishOrder.length === horses.length) setTimeout(finishRace, 500);
|
| 260 |
-
}
|
| 261 |
}
|
| 262 |
h.draw();
|
| 263 |
});
|
| 264 |
-
|
| 265 |
if(racing) updateRanking();
|
| 266 |
requestAnimationFrame(gameLoop);
|
| 267 |
}
|
| 268 |
|
| 269 |
function updateRanking() {
|
| 270 |
-
// Kombinierte Liste: Erst finishOrder (wer im Ziel ist), dann Rest nach relX
|
| 271 |
let displayList = [];
|
| 272 |
finishOrder.forEach(idx => displayList.push(horses[idx]));
|
| 273 |
let stillRacing = horses.filter(h => !h.finished).sort((a,b) => b.relX - a.relX);
|
| 274 |
displayList = displayList.concat(stillRacing);
|
| 275 |
-
|
| 276 |
document.getElementById('rankingList').innerHTML = displayList.map((h, i) => `
|
| 277 |
-
<div class="flex justify-between gap-2">
|
| 278 |
-
<span>${i+1}. ${h.name}</span>
|
| 279 |
-
<span class="${h.finished ? 'text-yellow-500' : 'opacity-50'}">${h.finished ? 'ZIEL' : Math.round(h.relX*100)+'%'}</span>
|
| 280 |
-
</div>
|
| 281 |
`).join('');
|
| 282 |
}
|
| 283 |
|
| 284 |
function finishRace() {
|
| 285 |
if(!racing) return; racing = false;
|
| 286 |
-
const
|
| 287 |
-
const winner = horses[winnerIdx];
|
| 288 |
raceHistory.unshift(winner.color); if(raceHistory.length > 5) raceHistory.pop();
|
| 289 |
document.getElementById('historyDots').innerHTML = raceHistory.map(c => `<div class="w-2 h-2 rounded-full border border-white" style="background:${c}"></div>`).join('');
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
document.getElementById('soundHumans').pause();
|
| 293 |
-
document.getElementById('soundWinner').play().catch(()=>{});
|
| 294 |
-
|
| 295 |
-
let won = false;
|
| 296 |
-
if (betType === 'win' && betOn === finishOrder[0]) won = true;
|
| 297 |
-
else if (betType === 'place' && finishOrder.slice(0, 3).includes(betOn)) won = true;
|
| 298 |
-
else if (betType === 'quartet' && finishOrder.slice(0, 4).includes(betOn)) won = true;
|
| 299 |
-
|
| 300 |
if(won) money += Math.floor(betAmount * MULTIPLIERS[betType]);
|
| 301 |
document.getElementById('winnerLargeImg').src = winner.img.src;
|
| 302 |
document.getElementById('overlay').style.display = 'block';
|
| 303 |
document.getElementById('modalTitle').innerText = won ? "SIEG!" : "ENDE";
|
| 304 |
document.getElementById('modalMessage').innerText = winner.name + " gewinnt!";
|
| 305 |
-
updateUI();
|
| 306 |
-
socket.emit('update_money', { email: userEmail, money: money });
|
| 307 |
}
|
| 308 |
|
| 309 |
-
function renderBetOptions() {
|
| 310 |
-
|
| 311 |
-
<div class="bet-circle" style="background: ${d.rgb}" onclick="selectHorse(${i})" id="horse-${i}"></div>
|
| 312 |
-
`).join('');
|
| 313 |
-
}
|
| 314 |
-
function selectHorse(index) {
|
| 315 |
-
if(racing || document.getElementById('overlay').style.display === 'block') return;
|
| 316 |
-
betOn = index;
|
| 317 |
-
document.querySelectorAll('.bet-circle').forEach(el => el.classList.remove('selected'));
|
| 318 |
-
document.getElementById(`horse-${index}`).classList.add('selected');
|
| 319 |
-
socket.emit('player_ready', { email: userEmail });
|
| 320 |
-
}
|
| 321 |
function changeBet(val) { if(!racing) { betAmount = Math.max(10, Math.min(money, betAmount + val)); updateUI(); } }
|
| 322 |
function updateUI() { document.getElementById('bankDisplay').innerText = money + "$"; document.getElementById('betAmount').innerText = betAmount + "$"; }
|
| 323 |
function setBetType(type) { if(!racing) { betType = type; document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active')); document.getElementById('btn-' + type).classList.add('active'); } }
|
| 324 |
function requestStart() { if(isAdmin && !racing) socket.emit('start_race', { email: userEmail }); }
|
| 325 |
function setReady() { document.getElementById('overlay').style.display = 'none'; betOn = null; renderBetOptions(); finishOrder = []; }
|
| 326 |
|
| 327 |
-
socket.on('ready_update', (data) => {
|
| 328 |
-
if(isAdmin) {
|
| 329 |
-
const btn = document.getElementById('startRace');
|
| 330 |
-
btn.disabled = (data.ready === 0);
|
| 331 |
-
btn.innerText = `START (${data.ready}/${data.total})`;
|
| 332 |
-
}
|
| 333 |
-
});
|
| 334 |
-
|
| 335 |
gameLoop();
|
| 336 |
</script>
|
| 337 |
</body>
|
|
|
|
| 8 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
|
| 9 |
<style>
|
| 10 |
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
| 11 |
+
html, body { margin: 0; padding: 0; width: 100%; height: 100dvh; background-color: #050a05; overflow: hidden; font-family: sans-serif; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
@keyframes gallop { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
| 13 |
.animate-gallop { animation: gallop 0.6s infinite; }
|
| 14 |
.btn-effect { box-shadow: 0 4px 15px rgba(229, 62, 62, 0.5); transition: all 0.2s; }
|
| 15 |
.btn-training { border: 2px solid #FFC72C; color: #FFC72C; transition: all 0.2s; }
|
| 16 |
.btn-training:hover { background: #FFC72C; color: black; }
|
| 17 |
+
#gameWrapper { display: none; flex-direction: column; height: 100dvh; width: 100%; }
|
| 18 |
+
#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; }
|
| 19 |
+
canvas { display: block; background: #1a471a; border: 2px solid #000; z-index: 5; box-shadow: 0 0 20px rgba(0,0,0,0.5); }
|
| 20 |
+
#ui { background: #000; padding: 12px; border-top: 3px solid #32CD32; z-index: 20; text-align: center; flex-shrink: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
.bet-circle { display: inline-block; width: 42px; height: 42px; border-radius: 50%; cursor: pointer; border: 3px solid transparent; transition: 0.2s; margin: 2px; }
|
| 22 |
.selected { border-color: #FFC72C !important; transform: scale(1.1); box-shadow: 0 0 15px #FFC72C; position: relative; }
|
| 23 |
.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; }
|
|
|
|
| 24 |
#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; }
|
|
|
|
| 25 |
#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; }
|
| 26 |
#chatMessages { flex-grow: 1; overflow-y: auto; padding: 5px; font-size: 10px; text-align: left; }
|
| 27 |
#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; }
|
|
|
|
| 28 |
.type-btn { background: #222; border: 1px solid #444; padding: 6px 12px; border-radius: 5px; font-size: 11px; font-weight: bold; cursor: pointer; color: white; }
|
| 29 |
.type-btn.active { background: #FFC72C; color: black; border-color: #fff; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
#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; }
|
| 31 |
</style>
|
| 32 |
</head>
|
|
|
|
| 34 |
|
| 35 |
<audio id="soundStart" src="assets/start.mp3"></audio>
|
| 36 |
<audio id="soundRace" src="assets/trap.mp3" loop></audio>
|
| 37 |
+
<audio id="soundHumans" src="assets/humans.mp3" loop></audio>
|
| 38 |
<audio id="soundWinner" src="assets/winner.mp3"></audio>
|
| 39 |
|
| 40 |
<div id="loginOverlay" class="fixed inset-0 flex items-center justify-center p-4 bg-[#050a05] z-[200]">
|
| 41 |
<div class="w-full max-w-xl bg-black rounded-xl p-8 border-4 border-yellow-500/70 text-center">
|
| 42 |
<div class="inline-block mb-4 animate-gallop"><img src="assets/ping.png" alt="Logo" width="80"></div>
|
| 43 |
<h1 class="text-3xl md:text-5xl font-extrabold text-yellow-500 mb-4 uppercase">Pferderennen 1920</h1>
|
|
|
|
| 44 |
<div class="space-y-4">
|
| 45 |
<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">
|
|
|
|
| 46 |
<div class="flex flex-col gap-3">
|
| 47 |
+
<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>
|
| 48 |
+
<a href="https://www.gamerjam.de/game/rrg/renrace.html" class="w-full btn-training text-lg font-bold py-3 rounded-full uppercase flex items-center justify-center">Übungsrennen (Training)</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
</div>
|
| 50 |
</div>
|
| 51 |
<p class="mt-6 text-gray-500 text-xs italic">GamerJam - Die goldene Ära des Turf</p>
|
|
|
|
| 60 |
</div>
|
| 61 |
|
| 62 |
<div id="canvasContainer">
|
| 63 |
+
<div id="rankingTable"><div id="rankingList" class="text-[10px]"></div></div>
|
| 64 |
+
<div id="chatBox"><div id="chatMessages"></div><input type="text" id="chatInput" placeholder="Chat..." onkeypress="handleChat(event)"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
<canvas id="gameCanvas" width="1200" height="700"></canvas>
|
|
|
|
| 66 |
<div id="overlay">
|
| 67 |
<h2 id="modalTitle" class="text-xl font-bold text-yellow-500 mb-2 uppercase">ERGEBNIS</h2>
|
| 68 |
<img id="winnerLargeImg" src="" class="w-16 h-16 mx-auto mb-2 object-contain">
|
|
|
|
| 77 |
<button onclick="setBetType('place')" id="btn-place" class="type-btn">PLATZ (x2.5)</button>
|
| 78 |
<button onclick="setBetType('quartet')" id="btn-quartet" class="type-btn">TOP 4 (x1.8)</button>
|
| 79 |
</div>
|
|
|
|
| 80 |
<div class="flex justify-center items-center gap-6 mb-3">
|
| 81 |
<div class="text-xl font-bold text-green-500" id="bankDisplay">1000$</div>
|
| 82 |
+
<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>
|
|
|
|
|
|
|
| 83 |
<div class="flex items-center gap-2">
|
| 84 |
<button onclick="changeBet(-10)" class="bg-gray-800 w-8 h-8 rounded-full font-bold">-</button>
|
| 85 |
<span id="betAmount" class="text-lg font-bold">50$</span>
|
|
|
|
| 94 |
const socket = io();
|
| 95 |
const canvas = document.getElementById('gameCanvas');
|
| 96 |
const ctx = canvas.getContext('2d');
|
| 97 |
+
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"}];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
const MULTIPLIERS = { win: 6, place: 2.5, quartet: 1.8 };
|
| 99 |
let money = 1000, betAmount = 50, betOn = null, betType = 'win', racing = false, userEmail = "", isAdmin = false, raceHistory = [];
|
| 100 |
let finishOrder = [];
|
|
|
|
| 103 |
const container = document.getElementById('canvasContainer');
|
| 104 |
if (!container) return;
|
| 105 |
const ratio = 1200 / 700;
|
| 106 |
+
let w = container.clientWidth - 20, h = w / ratio;
|
|
|
|
| 107 |
if (h > container.clientHeight - 20) { h = container.clientHeight - 20; w = h * ratio; }
|
| 108 |
canvas.style.width = w + "px"; canvas.style.height = h + "px";
|
| 109 |
}
|
|
|
|
| 123 |
socket.emit('join_race', { email: userEmail });
|
| 124 |
}
|
| 125 |
|
| 126 |
+
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 = ''; } } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
socket.on('new_chat_message', (data) => {
|
| 128 |
const chat = document.getElementById('chatMessages');
|
| 129 |
+
chat.innerHTML += `<div><span class="text-yellow-500 font-bold">${data.email.split('@')[0]}:</span> ${data.message}</div>`;
|
|
|
|
| 130 |
chat.scrollTop = chat.scrollHeight;
|
| 131 |
});
|
| 132 |
|
| 133 |
class Horse {
|
| 134 |
+
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; }
|
|
|
|
|
|
|
|
|
|
| 135 |
draw() {
|
| 136 |
const x = this.relX * 1200, y = (0.2 + this.index * 0.12) * 700;
|
| 137 |
+
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(); }
|
|
|
|
|
|
|
| 138 |
const bounce = (racing && !this.finished) ? Math.sin(Date.now()/110)*2.5 : 0;
|
| 139 |
if(this.img.complete) ctx.drawImage(this.img, x-50, y-50+bounce, 100, 100);
|
| 140 |
}
|
| 141 |
}
|
| 142 |
const horses = COLOR_DATA.map((d, i) => new Horse(i, d));
|
| 143 |
|
| 144 |
+
// ADMIN UPDATE: Erkennt Logouts und Ready-Status sofort
|
| 145 |
+
socket.on('ready_update', (data) => {
|
| 146 |
+
if(isAdmin) {
|
| 147 |
+
const btn = document.getElementById('startRace');
|
| 148 |
+
btn.disabled = (data.ready === 0);
|
| 149 |
+
btn.innerText = `START (${data.ready}/${data.total} Live)`;
|
| 150 |
+
btn.style.backgroundColor = (data.ready > 0) ? "#dc2626" : "#444";
|
| 151 |
+
}
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
socket.on('race_started', (data) => {
|
| 155 |
racing = true; finishOrder = [];
|
| 156 |
if(betOn !== null) money -= betAmount;
|
|
|
|
| 163 |
|
| 164 |
function gameLoop() {
|
| 165 |
ctx.clearRect(0,0,1200,700);
|
| 166 |
+
// Gestreifte Ziellinie
|
| 167 |
+
const fX = 1050, fW = 40;
|
| 168 |
+
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); } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
horses.forEach((h, i) => {
|
| 171 |
const y = (0.2 + i * 0.12) * 700;
|
| 172 |
ctx.fillStyle = h.color + "1A"; ctx.fillRect(0, y-40, 1200, 80);
|
| 173 |
+
if(racing && !h.finished) {
|
| 174 |
+
h.relX += 0.0022 * h.speedMult;
|
| 175 |
+
if(h.relX >= 0.865) { h.relX = 0.865; h.finished = true; finishOrder.push(h.index); if (finishOrder.length === horses.length) setTimeout(finishRace, 500); }
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
h.draw();
|
| 178 |
});
|
|
|
|
| 179 |
if(racing) updateRanking();
|
| 180 |
requestAnimationFrame(gameLoop);
|
| 181 |
}
|
| 182 |
|
| 183 |
function updateRanking() {
|
|
|
|
| 184 |
let displayList = [];
|
| 185 |
finishOrder.forEach(idx => displayList.push(horses[idx]));
|
| 186 |
let stillRacing = horses.filter(h => !h.finished).sort((a,b) => b.relX - a.relX);
|
| 187 |
displayList = displayList.concat(stillRacing);
|
|
|
|
| 188 |
document.getElementById('rankingList').innerHTML = displayList.map((h, i) => `
|
| 189 |
+
<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>
|
|
|
|
|
|
|
|
|
|
| 190 |
`).join('');
|
| 191 |
}
|
| 192 |
|
| 193 |
function finishRace() {
|
| 194 |
if(!racing) return; racing = false;
|
| 195 |
+
const winner = horses[finishOrder[0]];
|
|
|
|
| 196 |
raceHistory.unshift(winner.color); if(raceHistory.length > 5) raceHistory.pop();
|
| 197 |
document.getElementById('historyDots').innerHTML = raceHistory.map(c => `<div class="w-2 h-2 rounded-full border border-white" style="background:${c}"></div>`).join('');
|
| 198 |
+
document.getElementById('soundRace').pause(); document.getElementById('soundHumans').pause(); document.getElementById('soundWinner').play().catch(()=>{});
|
| 199 |
+
let won = (betType === 'win' && betOn === finishOrder[0]) || (betType === 'place' && finishOrder.slice(0, 3).includes(betOn)) || (betType === 'quartet' && finishOrder.slice(0, 4).includes(betOn));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
if(won) money += Math.floor(betAmount * MULTIPLIERS[betType]);
|
| 201 |
document.getElementById('winnerLargeImg').src = winner.img.src;
|
| 202 |
document.getElementById('overlay').style.display = 'block';
|
| 203 |
document.getElementById('modalTitle').innerText = won ? "SIEG!" : "ENDE";
|
| 204 |
document.getElementById('modalMessage').innerText = winner.name + " gewinnt!";
|
| 205 |
+
updateUI(); socket.emit('update_money', { email: userEmail, money: money });
|
|
|
|
| 206 |
}
|
| 207 |
|
| 208 |
+
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(''); }
|
| 209 |
+
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 }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
function changeBet(val) { if(!racing) { betAmount = Math.max(10, Math.min(money, betAmount + val)); updateUI(); } }
|
| 211 |
function updateUI() { document.getElementById('bankDisplay').innerText = money + "$"; document.getElementById('betAmount').innerText = betAmount + "$"; }
|
| 212 |
function setBetType(type) { if(!racing) { betType = type; document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active')); document.getElementById('btn-' + type).classList.add('active'); } }
|
| 213 |
function requestStart() { if(isAdmin && !racing) socket.emit('start_race', { email: userEmail }); }
|
| 214 |
function setReady() { document.getElementById('overlay').style.display = 'none'; betOn = null; renderBetOptions(); finishOrder = []; }
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
gameLoop();
|
| 217 |
</script>
|
| 218 |
</body>
|