Renday commited on
Commit
b0c0cb8
·
verified ·
1 Parent(s): 940dd25

Update templates/index.html

Browse files
Files changed (1) hide show
  1. 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
- #gameWrapper {
24
- display: none;
25
- flex-direction: column;
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.wave" loop></audio>
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
- Jetzt Live Wetten
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
- <div id="rankingList" class="text-[10px]"></div>
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
- const name = data.email.split('@')[0];
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
- // GESTREIFTE ZIELLINIE ZEICHNEN
244
- const fX = 1050; const fW = 40;
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) { // Ziellinie berührt
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 winnerIdx = finishOrder[0];
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
- document.getElementById('soundRace').pause();
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
- document.getElementById('betOptions').innerHTML = COLOR_DATA.map((d, i) => `
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>