OrbitMC commited on
Commit
27ccbef
·
verified ·
1 Parent(s): 2aad5fb

Update public/index.html

Browse files
Files changed (1) hide show
  1. public/index.html +126 -154
public/index.html CHANGED
@@ -25,9 +25,6 @@
25
  .powerup-badge { background: rgba(0,0,0,0.5); padding: 5px 10px; border-radius: 15px; color: #fff; font-size: 12px; font-weight: bold; display: none; }
26
  #tutorial { position: absolute; top: 40%; width: 100%; text-align: center; color: rgba(255,255,255,0.6); font-size: 18px; font-weight: 600; animation: pulse 2s infinite; }
27
 
28
- /* BUTTONS */
29
- #pause-btn { position: absolute; top: 20px; right: 20px; font-size: 24px; color: white; pointer-events: auto; cursor: pointer; z-index: 20; opacity: 0.5; }
30
-
31
  /* MENUS */
32
  .overlay-menu { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(18, 14, 30, 0.95); flex-direction: column; justify-content: center; align-items: center; pointer-events: auto; z-index: 50; }
33
  .overlay-menu h1 { color: #fff; font-size: 40px; margin-bottom: 20px; text-transform: uppercase; font-style: italic; }
@@ -39,24 +36,18 @@
39
  #login-screen { display: flex; z-index: 100; }
40
  #username-input { padding: 15px; font-size: 18px; border-radius: 10px; border: none; margin-bottom: 20px; text-align: center; width: 80%; max-width: 300px; font-family: inherit; }
41
 
42
- /* LEADERBOARD - TOP LEFT, SMALL */
43
  #leaderboard {
44
- position: absolute;
45
- top: 20px;
46
- left: 10px;
47
- background: rgba(0,0,0,0.5);
48
- padding: 8px 12px;
49
- border-radius: 10px;
50
- color: white;
51
- font-size: 11px;
52
- pointer-events: none;
53
- width: 130px;
54
- backdrop-filter: blur(4px);
55
  }
56
- .lb-title { color:#aaa; font-size:9px; margin-bottom:4px; text-transform: uppercase; letter-spacing: 1px;}
57
- .lb-row { display: flex; justify-content: space-between; margin-bottom: 3px; }
58
- .lb-rank { color: #ffd700; font-weight: bold; margin-right: 5px; width: 15px;}
59
- .lb-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60px; }
60
 
61
  @keyframes pulse { 0% { opacity: 0.4; transform: translateY(0); } 50% { opacity: 0.8; transform: translateY(-5px); } 100% { opacity: 0.4; transform: translateY(0); } }
62
  </style>
@@ -67,12 +58,9 @@
67
  <canvas id="gameCanvas"></canvas>
68
 
69
  <div id="ui-layer">
70
- <div id="pause-btn">❚❚</div>
71
-
72
- <!-- LEADERBOARD UI -->
73
  <div id="leaderboard">
74
- <div class="lb-title">Top Climbers</div>
75
- <div id="lb-content">Waiting...</div>
76
  </div>
77
 
78
  <div id="score-container">
@@ -96,7 +84,7 @@
96
  <button class="btn" id="start-btn">PLAY</button>
97
  </div>
98
 
99
- <!-- GAME OVER MENU (NO SHOP) -->
100
  <div id="game-over" class="overlay-menu">
101
  <h1>Fell Down!</h1>
102
  <p>Best Height: <span id="final-score">0</span>m</p>
@@ -107,10 +95,6 @@
107
 
108
  <script src="/socket.io/socket.io.js"></script>
109
  <script>
110
- /**
111
- * ASCENT: MULTIPLAYER EDITION (OPTIMIZED)
112
- */
113
-
114
  // --- SOCKET & NETWORK ---
115
  const socket = io();
116
  let myId = null;
@@ -124,7 +108,7 @@ const usernameInput = document.getElementById('username-input');
124
  const startBtn = document.getElementById('start-btn');
125
  const lbContent = document.getElementById('lb-content');
126
 
127
- let myName = localStorage.getItem('ascent_name') || `Player${Math.floor(Math.random()*999)}`;
128
  usernameInput.value = myName;
129
 
130
  startBtn.onclick = () => {
@@ -135,52 +119,49 @@ startBtn.onclick = () => {
135
 
136
  // Join Server
137
  socket.emit('join', { name: myName, skin: state.currentSkin });
138
- initLevel();
139
  state.isPlaying = true;
140
  gameLoop();
141
  };
142
 
143
- socket.on('connect', () => {
144
- myId = socket.id;
145
- });
146
 
147
- socket.on('config', (data) => {
148
- serverSeed = data.seed;
149
- });
150
-
151
- socket.on('worldUpdate', (data) => {
152
- // Sync other players
153
- data.players.forEach(p => {
154
- if (p.id !== socket.id) {
155
- if (!otherPlayers[p.id]) {
156
- // New Ghost
157
- otherPlayers[p.id] = {
158
- x: p.x, y: p.y, // Current render pos
159
- targetX: p.x, targetY: p.y, // Target pos
160
- skin: p.s,
161
- isDead: p.d,
162
- name: p.n || "Guest" // Get name from server
163
  };
164
  } else {
165
- // Existing Ghost - Update Target
166
- const op = otherPlayers[p.id];
167
- op.targetX = p.x;
168
- op.targetY = p.y;
169
- op.isDead = p.d;
170
- op.skin = p.s;
171
- if(p.n) op.name = p.n; // Ensure name is current
172
  }
173
  }
174
  });
175
 
176
- // Remove disconnected
177
- const currentIds = data.players.map(p => p.id);
178
  for (let id in otherPlayers) {
179
  if (!currentIds.includes(id)) delete otherPlayers[id];
180
  }
181
 
182
- // Update Leaderboard UI
183
- leaderboardData = data.lb;
184
  updateLeaderboardUI();
185
  });
186
 
@@ -195,30 +176,30 @@ function updateLeaderboardUI() {
195
  lbContent.innerHTML = html;
196
  }
197
 
198
- // Send Update Loop (High Rate for Smoothness)
199
  setInterval(() => {
200
- if (state.isPlaying && !state.gameOver) {
201
- socket.emit('update', {
202
- x: player.x,
203
- y: player.y,
204
- vx: player.vx,
205
- vy: player.vy,
206
- skin: state.currentSkin,
207
- score: state.highScore,
208
- isDead: false
209
- });
210
- } else if (state.gameOver) {
211
- socket.emit('update', { x:0, y:0, vx:0, vy:0, skin:state.currentSkin, score:state.highScore, isDead:true });
212
  }
213
- }, 16); // ~60 TPS
214
-
215
- // --- DETERMINISTIC RNG (For Syncing Level) ---
216
- function seededRandom(y, salt) {
217
- const v = Math.sin(y * 12.9898 + serverSeed + salt) * 43758.5453;
218
- return v - Math.floor(v);
 
 
 
219
  }
220
 
221
-
222
  // --- AUDIO SYSTEM ---
223
  const AudioSys = {
224
  ctx: null,
@@ -249,7 +230,7 @@ const AudioSys = {
249
  }
250
  };
251
 
252
- // --- CONSTANTS & CONFIG ---
253
  const LOGICAL_WIDTH = 375;
254
  const LOGICAL_HEIGHT = 812;
255
  const GRAVITY = 0.5;
@@ -261,7 +242,6 @@ const MAX_POWER = 22;
261
  const MIN_POWER = 3;
262
  const CAMERA_SMOOTHING = 0.1;
263
 
264
- // --- DYNAMIC COLORS ---
265
  const PALETTES = [
266
  { h: 0, top: '#2d2347', bot: '#7a5a8a' },
267
  { h: 1000, top: '#001a33', bot: '#006699' },
@@ -285,21 +265,20 @@ const uiBadgeSlow = document.getElementById('badge-slow');
285
  let state = {
286
  width: LOGICAL_WIDTH, height: LOGICAL_HEIGHT, scale: 1, cameraY: 0,
287
  score: 0, starsCollected: parseInt(localStorage.getItem('ascent_stars')) || 0,
288
- unlockedSkins: JSON.parse(localStorage.getItem('ascent_skins')) || ['square'],
289
  currentSkin: localStorage.getItem('ascent_current_skin') || 'square',
290
  highScore: 0, isPlaying: false, isPaused: false, gameOver: false,
291
- lastPlatformY: 0, timeScale: 1.0,
 
292
  hasShield: false, rocketActive: false, rocketTimer: 0, slowMoTimer: 0,
293
  isDragging: false, dragStartX: 0, dragStartY: 0, dragCurrX: 0, dragCurrY: 0,
294
  bgTop: PALETTES[0].top, bgBot: PALETTES[0].bot
295
  };
296
 
297
- // --- SKINS DATA (Simplified) ---
298
  const SKINS = [
299
- { id: 'square', name: 'Cube', price: 0, color: '#fff' },
300
- { id: 'circle', name: 'Ball', price: 50, color: '#00ff88' },
301
- { id: 'triangle', name: 'Tri', price: 100, color: '#ff00ff' },
302
- { id: 'gold', name: 'Gold', price: 300, color: '#ffd700' }
303
  ];
304
 
305
  // --- ENTITIES ---
@@ -351,20 +330,15 @@ class Player {
351
 
352
  ctx.translate(dX + this.w/2, dY + this.h/2);
353
 
354
- if (!isLocal) ctx.globalAlpha = 0.5;
355
 
356
  if (state.rocketActive && isLocal) {
357
  ctx.fillStyle = '#ff4444';
358
  ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(10, 10); ctx.lineTo(-10, 10); ctx.fill();
359
- ctx.fillStyle = `rgba(255, 200, 0, ${Math.random()})`;
360
- ctx.beginPath(); ctx.arc(0, 15, Math.random()*10, 0, Math.PI*2); ctx.fill();
361
  ctx.restore(); return;
362
  }
363
 
364
- if (isLocal) {
365
- ctx.rotate(this.rotation);
366
- ctx.scale(this.stretchX, this.stretchY);
367
- }
368
 
369
  const skinId = customSkin || state.currentSkin;
370
  const skinData = SKINS.find(s => s.id === skinId) || SKINS[0];
@@ -380,9 +354,7 @@ class Player {
380
  if (isLocal) {
381
  if (Math.abs(this.vx) > 1) { const dir = Math.sign(this.vx); ctx.fillRect(dir * 4 - 2, -4, 4, 8); }
382
  else { ctx.fillRect(-5, -2, 3, 3); ctx.fillRect(3, -2, 3, 3); }
383
- } else {
384
- ctx.fillRect(-5, -2, 3, 3); ctx.fillRect(3, -2, 3, 3);
385
- }
386
 
387
  if (state.hasShield && isLocal) { ctx.strokeStyle = "#00ffff"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, this.w, 0, Math.PI*2); ctx.stroke(); }
388
 
@@ -402,11 +374,13 @@ class Platform {
402
  this.x = x; this.y = y; this.w = w; this.h = h;
403
  this.type = type;
404
  this.crumbleTimer = 60; this.isCrumbling = false; this.isDestroyed = false; this.hasBounced = false;
405
- const r1 = seededRandom(y, 1);
 
406
  this.vx = (type === 'moving') ? (r1 > 0.5 ? 1 : -1) * (1 + r1 * 1.5) : 0;
407
- const r2 = seededRandom(y, 2);
 
408
  this.hasSpikes = (r2 < 0.2 && type !== 'crumble' && type !== 'bouncy');
409
- this.spikeX = seededRandom(y, 3) * (this.w - 20);
410
  this.decorations = [];
411
  this.generateDecorations();
412
  }
@@ -425,9 +399,8 @@ class Platform {
425
  }
426
  generateDecorations() {
427
  if (this.type === 'crumble' || this.type === 'ice' || this.type === 'bouncy') return;
428
- const r = seededRandom(this.y, 4);
429
- if (r > 0.5) this.decorations.push({ type: 'vine', x: seededRandom(this.y,5)*(this.w-10), len: 20+seededRandom(this.y,6)*40 });
430
- if (seededRandom(this.y, 7) > 0.7 && !this.hasSpikes) this.decorations.push({ type: 'tree', x: seededRandom(this.y,8)*(this.w-20), h: 20+seededRandom(this.y,9)*30 });
431
  }
432
  draw(ctx) {
433
  if (this.isDestroyed) return;
@@ -445,19 +418,14 @@ class Platform {
445
  ctx.beginPath(); ctx.roundRect(dx, dy, this.w, this.h, 8); ctx.fill();
446
  if (this.type === 'bouncy') {
447
  ctx.fillStyle = !this.hasBounced ? '#ff66cc' : '#554466'; ctx.fillRect(dx+5, !this.hasBounced?dy:dy+4, this.w-10, !this.hasBounced?6:2);
448
- } else if (this.type === 'ice') {
449
- ctx.fillStyle = '#00ffff'; ctx.fillRect(dx, dy, this.w, 8);
450
- } else {
451
- ctx.fillStyle = COLORS.platformHighlight; ctx.beginPath(); ctx.roundRect(dx+4, dy+4, this.w-8, this.h-8, 4); ctx.fill();
452
- }
453
- if (this.hasSpikes) {
454
- ctx.fillStyle = COLORS.spike; ctx.beginPath(); ctx.moveTo(dx + this.spikeX, dy); ctx.lineTo(dx + this.spikeX + 10, dy - 15); ctx.lineTo(dx + this.spikeX + 20, dy); ctx.fill();
455
- }
456
  }
457
  }
458
 
459
  class Enemy {
460
- constructor(y) { this.y = y; this.x = seededRandom(y, 10) * LOGICAL_WIDTH; this.r = 15; this.vx = 2; this.angle = 0; }
461
  update() { this.x += this.vx * state.timeScale; if (this.x < 15 || this.x > LOGICAL_WIDTH-15) this.vx *= -1; this.angle += 0.2 * state.timeScale; }
462
  draw(ctx) {
463
  ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
@@ -467,23 +435,6 @@ class Enemy {
467
  }
468
  }
469
 
470
- class WindZone {
471
- constructor(y) {
472
- this.y = y; this.h = 100;
473
- this.dir = seededRandom(y, 11) > 0.5 ? 1 : -1;
474
- this.particles = [];
475
- for(let i=0; i<10; i++) this.particles.push({x:Math.random()*LOGICAL_WIDTH, y:Math.random()*this.h, s:Math.random()*3+2});
476
- }
477
- update() {
478
- this.particles.forEach(p => { p.x += this.dir * 4 * state.timeScale; if(p.x > LOGICAL_WIDTH) p.x = 0; if(p.x < 0) p.x = LOGICAL_WIDTH; });
479
- if (player.y > this.y && player.y < this.y + this.h) player.vx += this.dir * 0.5 * state.timeScale;
480
- }
481
- draw(ctx) {
482
- ctx.fillStyle = COLORS.wind;
483
- this.particles.forEach(p => { ctx.fillRect(p.x, this.y + p.y, p.s*2, 2); });
484
- }
485
- }
486
-
487
  class Collectible {
488
  constructor(x, y, type) { this.x = x; this.y = y; this.type = type; this.collected = false; this.bob = Math.random() * Math.PI; }
489
  draw(ctx) {
@@ -502,12 +453,29 @@ class Collectible {
502
  }
503
  }
504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  const player = new Player();
506
  let platforms = [], enemies = [], winds = [], collectibles = [], particles = [];
507
 
508
  function initLevel() {
509
  platforms = []; enemies = []; winds = []; collectibles = []; particles = [];
510
  state.lastPlatformY = LOGICAL_HEIGHT - 100;
 
 
 
511
  platforms.push(new Platform(0, LOGICAL_HEIGHT - 100, LOGICAL_WIDTH, 150));
512
  generateChunk(LOGICAL_HEIGHT - 250);
513
  }
@@ -517,37 +485,39 @@ function generateChunk(targetY) {
517
  let loops = 0;
518
  while (y > targetY && loops < 500) {
519
  loops++;
 
 
520
  const heightMeters = Math.abs(y - (LOGICAL_HEIGHT - 100)) / 10;
521
  const diff = Math.min(1, heightMeters / 1500);
522
 
523
- const rGap = seededRandom(y, 12);
524
  const minGap = 80 + (diff * 40);
525
  const maxGap = 160 + (diff * 60);
526
  y -= (minGap + rGap * (maxGap - minGap));
527
 
528
- let w = (60 + seededRandom(y, 13) * 100) * (1 - diff * 0.4);
529
  w = Math.max(40, w);
530
- let x = Math.max(10, Math.min(seededRandom(y, 14)*(LOGICAL_WIDTH-w), LOGICAL_WIDTH-w-10));
531
 
532
  let type = 'normal';
533
- const rand = seededRandom(y, 15);
534
  if (heightMeters > 50 && rand < 0.2 + diff*0.3) type = 'moving';
535
  else if (heightMeters > 20 && rand < 0.35) type = 'crumble';
536
  else if (heightMeters > 100 && rand < 0.45) type = 'bouncy';
537
  else if (heightMeters > 150 && rand < 0.55) type = 'ice';
538
 
539
- platforms.push(new Platform(x, y, w, 30 + seededRandom(y, 16) * 100, type));
540
 
541
- if (seededRandom(y, 17) < 0.05) {
542
  const pArr = ['rocket','shield','slow'];
543
- const pType = pArr[Math.floor(seededRandom(y, 18)*3)];
544
  collectibles.push(new Collectible(x+w/2, y-30, pType));
545
- } else if (seededRandom(y, 19) < 0.25) {
546
  collectibles.push(new Collectible(x+w/2, y-30-Math.random()*30, 'star'));
547
  }
548
 
549
- if (heightMeters > 200 && seededRandom(y, 20) < 0.1 + diff*0.3) enemies.push(new Enemy(y - 50));
550
- if (heightMeters > 300 && seededRandom(y, 21) < 0.1) winds.push(new WindZone(y - 100));
551
  }
552
  state.lastPlatformY = y;
553
  }
@@ -667,20 +637,17 @@ function gameLoop() {
667
  collectibles.forEach(c => { if(isVisible(c.y, 20)) c.draw(ctx); });
668
  enemies.forEach(e => { if(isVisible(e.y, 50)) e.draw(ctx); });
669
 
670
- // Draw Other Players (Ghosts with Interpolation)
671
  for (let id in otherPlayers) {
672
  const op = otherPlayers[id];
673
  if (!op.isDead && isVisible(op.y, 50)) {
674
- // LERP: Smooth movement
675
- op.x += (op.targetX - op.x) * 0.3;
676
- op.y += (op.targetY - op.y) * 0.3;
677
-
678
- player.draw(ctx, false, op.skin, op.x, op.y);
679
 
680
- // Draw Name
681
  ctx.fillStyle = "rgba(255,255,255,0.7)";
682
- ctx.font = "10px Arial";
683
- ctx.textAlign = "center";
684
  ctx.fillText(op.name, op.x + 12, op.y - 10);
685
  }
686
  }
@@ -703,8 +670,14 @@ function restartGame() {
703
  state.rocketActive = false; state.hasShield = false; state.slowMoTimer = 0;
704
  document.querySelectorAll('.powerup-badge').forEach(el => el.style.display = 'none');
705
  uiScore.innerText = "0"; uiGameOver.style.display = 'none';
706
- player.reset(); initLevel(); uiTutorial.style.display = 'block';
707
- socket.emit('update', { x:player.x, y:player.y, vx:0, vy:0, skin:state.currentSkin, score:0, isDead:false });
 
 
 
 
 
 
708
  }
709
 
710
  function getPointerPos(e) {
@@ -738,7 +711,6 @@ window.addEventListener('mousedown', startDrag); window.addEventListener('mousem
738
  window.addEventListener('touchstart', startDrag, {passive:false}); window.addEventListener('touchmove', moveDrag, {passive:false}); window.addEventListener('touchend', endDrag);
739
 
740
  document.getElementById('retry-btn').onclick = (e) => { e.stopPropagation(); restartGame(); };
741
- document.getElementById('pause-btn').onclick = (e) => { e.stopPropagation(); state.isPaused = !state.isPaused; };
742
 
743
  function drawTrajectory(ctx) {
744
  if (!state.isDragging || !player.grounded) return;
 
25
  .powerup-badge { background: rgba(0,0,0,0.5); padding: 5px 10px; border-radius: 15px; color: #fff; font-size: 12px; font-weight: bold; display: none; }
26
  #tutorial { position: absolute; top: 40%; width: 100%; text-align: center; color: rgba(255,255,255,0.6); font-size: 18px; font-weight: 600; animation: pulse 2s infinite; }
27
 
 
 
 
28
  /* MENUS */
29
  .overlay-menu { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(18, 14, 30, 0.95); flex-direction: column; justify-content: center; align-items: center; pointer-events: auto; z-index: 50; }
30
  .overlay-menu h1 { color: #fff; font-size: 40px; margin-bottom: 20px; text-transform: uppercase; font-style: italic; }
 
36
  #login-screen { display: flex; z-index: 100; }
37
  #username-input { padding: 15px; font-size: 18px; border-radius: 10px; border: none; margin-bottom: 20px; text-align: center; width: 80%; max-width: 300px; font-family: inherit; }
38
 
39
+ /* LEADERBOARD - Compact Top Left */
40
  #leaderboard {
41
+ position: absolute; top: 15px; left: 15px;
42
+ background: rgba(0,0,0,0.4);
43
+ padding: 8px 10px; border-radius: 8px;
44
+ color: white; font-size: 11px; width: 120px;
45
+ pointer-events: none; backdrop-filter: blur(2px);
 
 
 
 
 
 
46
  }
47
+ .lb-title { color:#aaa; font-size:9px; margin-bottom:4px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: bold;}
48
+ .lb-row { display: flex; justify-content: space-between; margin-bottom: 3px; align-items: center; }
49
+ .lb-rank { color: #ffd700; font-weight: bold; margin-right: 4px; width: 12px; }
50
+ .lb-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 55px; }
51
 
52
  @keyframes pulse { 0% { opacity: 0.4; transform: translateY(0); } 50% { opacity: 0.8; transform: translateY(-5px); } 100% { opacity: 0.4; transform: translateY(0); } }
53
  </style>
 
58
  <canvas id="gameCanvas"></canvas>
59
 
60
  <div id="ui-layer">
 
 
 
61
  <div id="leaderboard">
62
+ <div class="lb-title">Top Altitude</div>
63
+ <div id="lb-content">Connecting...</div>
64
  </div>
65
 
66
  <div id="score-container">
 
84
  <button class="btn" id="start-btn">PLAY</button>
85
  </div>
86
 
87
+ <!-- GAME OVER MENU -->
88
  <div id="game-over" class="overlay-menu">
89
  <h1>Fell Down!</h1>
90
  <p>Best Height: <span id="final-score">0</span>m</p>
 
95
 
96
  <script src="/socket.io/socket.io.js"></script>
97
  <script>
 
 
 
 
98
  // --- SOCKET & NETWORK ---
99
  const socket = io();
100
  let myId = null;
 
108
  const startBtn = document.getElementById('start-btn');
109
  const lbContent = document.getElementById('lb-content');
110
 
111
+ let myName = localStorage.getItem('ascent_name') || `Jumper${Math.floor(Math.random()*100)}`;
112
  usernameInput.value = myName;
113
 
114
  startBtn.onclick = () => {
 
119
 
120
  // Join Server
121
  socket.emit('join', { name: myName, skin: state.currentSkin });
122
+ initLevel(); // Now safe to init level as player is "logged in"
123
  state.isPlaying = true;
124
  gameLoop();
125
  };
126
 
127
+ socket.on('connect', () => { myId = socket.id; });
128
+ socket.on('config', (data) => { serverSeed = data.seed; });
 
129
 
130
+ // World Update (Optimized 'w' event)
131
+ socket.on('w', (data) => {
132
+ // Sync Ghosts
133
+ const currentIds = [];
134
+
135
+ data.p.forEach(pData => {
136
+ // [id, x, y, skin, isDead, name]
137
+ const pid = pData[0];
138
+ currentIds.push(pid);
139
+
140
+ if (pid !== socket.id) {
141
+ if (!otherPlayers[pid]) {
142
+ otherPlayers[pid] = {
143
+ x: pData[1], y: pData[2],
144
+ tx: pData[1], ty: pData[2],
145
+ skin: pData[3], isDead: pData[4], name: pData[5]
146
  };
147
  } else {
148
+ const op = otherPlayers[pid];
149
+ op.tx = pData[1];
150
+ op.ty = pData[2];
151
+ op.skin = pData[3];
152
+ op.isDead = pData[4];
153
+ op.name = pData[5];
 
154
  }
155
  }
156
  });
157
 
158
+ // Cleanup disconnected
 
159
  for (let id in otherPlayers) {
160
  if (!currentIds.includes(id)) delete otherPlayers[id];
161
  }
162
 
163
+ // Leaderboard
164
+ leaderboardData = data.l;
165
  updateLeaderboardUI();
166
  });
167
 
 
176
  lbContent.innerHTML = html;
177
  }
178
 
179
+ // Network Loop: 20 TPS (50ms) is enough for client upload. Prevents network flooding.
180
  setInterval(() => {
181
+ if (state.isPlaying || state.gameOver) {
182
+ // Send Array [x, y, vx, vy, score, isDead]
183
+ socket.emit('update', [
184
+ Math.round(player.x),
185
+ Math.round(player.y),
186
+ parseFloat(player.vx.toFixed(2)),
187
+ parseFloat(player.vy.toFixed(2)),
188
+ state.highScore,
189
+ state.gameOver ? 1 : 0
190
+ ]);
 
 
191
  }
192
+ }, 50);
193
+
194
+ // --- DETERMINISTIC RNG (FIXED LEVEL CONSISTENCY) ---
195
+ // We use 'state.platformCount' instead of 'y' to seed the random.
196
+ // This guarantees the sequence of platforms is identical every time.
197
+ function getSeededRandom(offset) {
198
+ const seed = serverSeed + (state.platformCount * 1337) + offset;
199
+ const x = Math.sin(seed) * 10000;
200
+ return x - Math.floor(x);
201
  }
202
 
 
203
  // --- AUDIO SYSTEM ---
204
  const AudioSys = {
205
  ctx: null,
 
230
  }
231
  };
232
 
233
+ // --- CONSTANTS ---
234
  const LOGICAL_WIDTH = 375;
235
  const LOGICAL_HEIGHT = 812;
236
  const GRAVITY = 0.5;
 
242
  const MIN_POWER = 3;
243
  const CAMERA_SMOOTHING = 0.1;
244
 
 
245
  const PALETTES = [
246
  { h: 0, top: '#2d2347', bot: '#7a5a8a' },
247
  { h: 1000, top: '#001a33', bot: '#006699' },
 
265
  let state = {
266
  width: LOGICAL_WIDTH, height: LOGICAL_HEIGHT, scale: 1, cameraY: 0,
267
  score: 0, starsCollected: parseInt(localStorage.getItem('ascent_stars')) || 0,
 
268
  currentSkin: localStorage.getItem('ascent_current_skin') || 'square',
269
  highScore: 0, isPlaying: false, isPaused: false, gameOver: false,
270
+ lastPlatformY: 0, platformCount: 0, // CRITICAL: Used for RNG seed
271
+ timeScale: 1.0,
272
  hasShield: false, rocketActive: false, rocketTimer: 0, slowMoTimer: 0,
273
  isDragging: false, dragStartX: 0, dragStartY: 0, dragCurrX: 0, dragCurrY: 0,
274
  bgTop: PALETTES[0].top, bgBot: PALETTES[0].bot
275
  };
276
 
 
277
  const SKINS = [
278
+ { id: 'square', color: '#fff' },
279
+ { id: 'circle', color: '#00ff88' },
280
+ { id: 'triangle', color: '#ff00ff' },
281
+ { id: 'gold', color: '#ffd700' }
282
  ];
283
 
284
  // --- ENTITIES ---
 
330
 
331
  ctx.translate(dX + this.w/2, dY + this.h/2);
332
 
333
+ if (!isLocal) ctx.globalAlpha = 0.6;
334
 
335
  if (state.rocketActive && isLocal) {
336
  ctx.fillStyle = '#ff4444';
337
  ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(10, 10); ctx.lineTo(-10, 10); ctx.fill();
 
 
338
  ctx.restore(); return;
339
  }
340
 
341
+ if (isLocal) { ctx.rotate(this.rotation); ctx.scale(this.stretchX, this.stretchY); }
 
 
 
342
 
343
  const skinId = customSkin || state.currentSkin;
344
  const skinData = SKINS.find(s => s.id === skinId) || SKINS[0];
 
354
  if (isLocal) {
355
  if (Math.abs(this.vx) > 1) { const dir = Math.sign(this.vx); ctx.fillRect(dir * 4 - 2, -4, 4, 8); }
356
  else { ctx.fillRect(-5, -2, 3, 3); ctx.fillRect(3, -2, 3, 3); }
357
+ } else { ctx.fillRect(-5, -2, 3, 3); ctx.fillRect(3, -2, 3, 3); }
 
 
358
 
359
  if (state.hasShield && isLocal) { ctx.strokeStyle = "#00ffff"; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, this.w, 0, Math.PI*2); ctx.stroke(); }
360
 
 
374
  this.x = x; this.y = y; this.w = w; this.h = h;
375
  this.type = type;
376
  this.crumbleTimer = 60; this.isCrumbling = false; this.isDestroyed = false; this.hasBounced = false;
377
+
378
+ const r1 = getSeededRandom(1);
379
  this.vx = (type === 'moving') ? (r1 > 0.5 ? 1 : -1) * (1 + r1 * 1.5) : 0;
380
+
381
+ const r2 = getSeededRandom(2);
382
  this.hasSpikes = (r2 < 0.2 && type !== 'crumble' && type !== 'bouncy');
383
+ this.spikeX = getSeededRandom(3) * (this.w - 20);
384
  this.decorations = [];
385
  this.generateDecorations();
386
  }
 
399
  }
400
  generateDecorations() {
401
  if (this.type === 'crumble' || this.type === 'ice' || this.type === 'bouncy') return;
402
+ if (getSeededRandom(4) > 0.5) this.decorations.push({ type: 'vine', x: getSeededRandom(5)*(this.w-10), len: 20+getSeededRandom(6)*40 });
403
+ if (getSeededRandom(7) > 0.7 && !this.hasSpikes) this.decorations.push({ type: 'tree', x: getSeededRandom(8)*(this.w-20), h: 20+getSeededRandom(9)*30 });
 
404
  }
405
  draw(ctx) {
406
  if (this.isDestroyed) return;
 
418
  ctx.beginPath(); ctx.roundRect(dx, dy, this.w, this.h, 8); ctx.fill();
419
  if (this.type === 'bouncy') {
420
  ctx.fillStyle = !this.hasBounced ? '#ff66cc' : '#554466'; ctx.fillRect(dx+5, !this.hasBounced?dy:dy+4, this.w-10, !this.hasBounced?6:2);
421
+ } else if (this.type === 'ice') { ctx.fillStyle = '#00ffff'; ctx.fillRect(dx, dy, this.w, 8); }
422
+ else { ctx.fillStyle = COLORS.platformHighlight; ctx.beginPath(); ctx.roundRect(dx+4, dy+4, this.w-8, this.h-8, 4); ctx.fill(); }
423
+ if (this.hasSpikes) { ctx.fillStyle = COLORS.spike; ctx.beginPath(); ctx.moveTo(dx + this.spikeX, dy); ctx.lineTo(dx + this.spikeX + 10, dy - 15); ctx.lineTo(dx + this.spikeX + 20, dy); ctx.fill(); }
 
 
 
 
 
424
  }
425
  }
426
 
427
  class Enemy {
428
+ constructor(y) { this.y = y; this.x = getSeededRandom(10) * LOGICAL_WIDTH; this.r = 15; this.vx = 2; this.angle = 0; }
429
  update() { this.x += this.vx * state.timeScale; if (this.x < 15 || this.x > LOGICAL_WIDTH-15) this.vx *= -1; this.angle += 0.2 * state.timeScale; }
430
  draw(ctx) {
431
  ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
 
435
  }
436
  }
437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  class Collectible {
439
  constructor(x, y, type) { this.x = x; this.y = y; this.type = type; this.collected = false; this.bob = Math.random() * Math.PI; }
440
  draw(ctx) {
 
453
  }
454
  }
455
 
456
+ class WindZone {
457
+ constructor(y) {
458
+ this.y = y; this.h = 100;
459
+ this.dir = getSeededRandom(11) > 0.5 ? 1 : -1;
460
+ this.particles = [];
461
+ for(let i=0; i<10; i++) this.particles.push({x:Math.random()*LOGICAL_WIDTH, y:Math.random()*this.h, s:Math.random()*3+2});
462
+ }
463
+ update() {
464
+ this.particles.forEach(p => { p.x += this.dir * 4 * state.timeScale; if(p.x > LOGICAL_WIDTH) p.x = 0; if(p.x < 0) p.x = LOGICAL_WIDTH; });
465
+ if (player.y > this.y && player.y < this.y + this.h) player.vx += this.dir * 0.5 * state.timeScale;
466
+ }
467
+ draw(ctx) { ctx.fillStyle = COLORS.wind; this.particles.forEach(p => { ctx.fillRect(p.x, this.y + p.y, p.s*2, 2); }); }
468
+ }
469
+
470
  const player = new Player();
471
  let platforms = [], enemies = [], winds = [], collectibles = [], particles = [];
472
 
473
  function initLevel() {
474
  platforms = []; enemies = []; winds = []; collectibles = []; particles = [];
475
  state.lastPlatformY = LOGICAL_HEIGHT - 100;
476
+ state.platformCount = 0; // RESET COUNT ON INIT
477
+
478
+ // Initial Platform
479
  platforms.push(new Platform(0, LOGICAL_HEIGHT - 100, LOGICAL_WIDTH, 150));
480
  generateChunk(LOGICAL_HEIGHT - 250);
481
  }
 
485
  let loops = 0;
486
  while (y > targetY && loops < 500) {
487
  loops++;
488
+ state.platformCount++; // Increment count for RNG consistency
489
+
490
  const heightMeters = Math.abs(y - (LOGICAL_HEIGHT - 100)) / 10;
491
  const diff = Math.min(1, heightMeters / 1500);
492
 
493
+ const rGap = getSeededRandom(12);
494
  const minGap = 80 + (diff * 40);
495
  const maxGap = 160 + (diff * 60);
496
  y -= (minGap + rGap * (maxGap - minGap));
497
 
498
+ let w = (60 + getSeededRandom(13) * 100) * (1 - diff * 0.4);
499
  w = Math.max(40, w);
500
+ let x = Math.max(10, Math.min(getSeededRandom(14)*(LOGICAL_WIDTH-w), LOGICAL_WIDTH-w-10));
501
 
502
  let type = 'normal';
503
+ const rand = getSeededRandom(15);
504
  if (heightMeters > 50 && rand < 0.2 + diff*0.3) type = 'moving';
505
  else if (heightMeters > 20 && rand < 0.35) type = 'crumble';
506
  else if (heightMeters > 100 && rand < 0.45) type = 'bouncy';
507
  else if (heightMeters > 150 && rand < 0.55) type = 'ice';
508
 
509
+ platforms.push(new Platform(x, y, w, 30 + getSeededRandom(16) * 100, type));
510
 
511
+ if (getSeededRandom(17) < 0.05) {
512
  const pArr = ['rocket','shield','slow'];
513
+ const pType = pArr[Math.floor(getSeededRandom(18)*3)];
514
  collectibles.push(new Collectible(x+w/2, y-30, pType));
515
+ } else if (getSeededRandom(19) < 0.25) {
516
  collectibles.push(new Collectible(x+w/2, y-30-Math.random()*30, 'star'));
517
  }
518
 
519
+ if (heightMeters > 200 && getSeededRandom(20) < 0.1 + diff*0.3) enemies.push(new Enemy(y - 50));
520
+ if (heightMeters > 300 && getSeededRandom(21) < 0.1) winds.push(new WindZone(y - 100));
521
  }
522
  state.lastPlatformY = y;
523
  }
 
637
  collectibles.forEach(c => { if(isVisible(c.y, 20)) c.draw(ctx); });
638
  enemies.forEach(e => { if(isVisible(e.y, 50)) e.draw(ctx); });
639
 
640
+ // Draw Other Players with Smooth Interpolation
641
  for (let id in otherPlayers) {
642
  const op = otherPlayers[id];
643
  if (!op.isDead && isVisible(op.y, 50)) {
644
+ // LERP: Move 20% towards target every frame
645
+ op.x += (op.tx - op.x) * 0.2;
646
+ op.y += (op.ty - op.y) * 0.2;
 
 
647
 
648
+ player.draw(ctx, false, op.skin, op.x, op.y);
649
  ctx.fillStyle = "rgba(255,255,255,0.7)";
650
+ ctx.font = "10px Arial"; ctx.textAlign = "center";
 
651
  ctx.fillText(op.name, op.x + 12, op.y - 10);
652
  }
653
  }
 
670
  state.rocketActive = false; state.hasShield = false; state.slowMoTimer = 0;
671
  document.querySelectorAll('.powerup-badge').forEach(el => el.style.display = 'none');
672
  uiScore.innerText = "0"; uiGameOver.style.display = 'none';
673
+
674
+ // Reset Player and Level (Will generate same level because platformCount resets)
675
+ player.reset();
676
+ initLevel();
677
+ uiTutorial.style.display = 'block';
678
+
679
+ // Notify Server
680
+ socket.emit('update', [Math.round(player.x), Math.round(player.y), 0, 0, 0, 0]);
681
  }
682
 
683
  function getPointerPos(e) {
 
711
  window.addEventListener('touchstart', startDrag, {passive:false}); window.addEventListener('touchmove', moveDrag, {passive:false}); window.addEventListener('touchend', endDrag);
712
 
713
  document.getElementById('retry-btn').onclick = (e) => { e.stopPropagation(); restartGame(); };
 
714
 
715
  function drawTrajectory(ctx) {
716
  if (!state.isDragging || !player.grounded) return;