Update public/index.html
Browse files- 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 -
|
| 43 |
#leaderboard {
|
| 44 |
-
position: absolute;
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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:
|
| 57 |
-
.lb-row { display: flex; justify-content: space-between; margin-bottom: 3px; }
|
| 58 |
-
.lb-rank { color: #ffd700; font-weight: bold; margin-right:
|
| 59 |
-
.lb-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width:
|
| 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
|
| 75 |
-
<div id="lb-content">
|
| 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
|
| 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') || `
|
| 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 |
-
|
| 145 |
-
});
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
};
|
| 164 |
} else {
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
op.
|
| 168 |
-
op.
|
| 169 |
-
op.isDead =
|
| 170 |
-
op.
|
| 171 |
-
if(p.n) op.name = p.n; // Ensure name is current
|
| 172 |
}
|
| 173 |
}
|
| 174 |
});
|
| 175 |
|
| 176 |
-
//
|
| 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 |
-
//
|
| 183 |
-
leaderboardData = data.
|
| 184 |
updateLeaderboardUI();
|
| 185 |
});
|
| 186 |
|
|
@@ -195,30 +176,30 @@ function updateLeaderboardUI() {
|
|
| 195 |
lbContent.innerHTML = html;
|
| 196 |
}
|
| 197 |
|
| 198 |
-
//
|
| 199 |
setInterval(() => {
|
| 200 |
-
if (state.isPlaying
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 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 |
-
},
|
| 214 |
-
|
| 215 |
-
// --- DETERMINISTIC RNG (
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
-
|
| 222 |
// --- AUDIO SYSTEM ---
|
| 223 |
const AudioSys = {
|
| 224 |
ctx: null,
|
|
@@ -249,7 +230,7 @@ const AudioSys = {
|
|
| 249 |
}
|
| 250 |
};
|
| 251 |
|
| 252 |
-
// --- CONSTANTS
|
| 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,
|
|
|
|
| 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',
|
| 300 |
-
{ id: 'circle',
|
| 301 |
-
{ id: 'triangle',
|
| 302 |
-
{ id: 'gold',
|
| 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.
|
| 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 |
-
|
|
|
|
| 406 |
this.vx = (type === 'moving') ? (r1 > 0.5 ? 1 : -1) * (1 + r1 * 1.5) : 0;
|
| 407 |
-
|
|
|
|
| 408 |
this.hasSpikes = (r2 < 0.2 && type !== 'crumble' && type !== 'bouncy');
|
| 409 |
-
this.spikeX =
|
| 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 |
-
|
| 429 |
-
if (
|
| 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 |
-
|
| 450 |
-
|
| 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 =
|
| 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 =
|
| 524 |
const minGap = 80 + (diff * 40);
|
| 525 |
const maxGap = 160 + (diff * 60);
|
| 526 |
y -= (minGap + rGap * (maxGap - minGap));
|
| 527 |
|
| 528 |
-
let w = (60 +
|
| 529 |
w = Math.max(40, w);
|
| 530 |
-
let x = Math.max(10, Math.min(
|
| 531 |
|
| 532 |
let type = 'normal';
|
| 533 |
-
const rand =
|
| 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 +
|
| 540 |
|
| 541 |
-
if (
|
| 542 |
const pArr = ['rocket','shield','slow'];
|
| 543 |
-
const pType = pArr[Math.floor(
|
| 544 |
collectibles.push(new Collectible(x+w/2, y-30, pType));
|
| 545 |
-
} else if (
|
| 546 |
collectibles.push(new Collectible(x+w/2, y-30-Math.random()*30, 'star'));
|
| 547 |
}
|
| 548 |
|
| 549 |
-
if (heightMeters > 200 &&
|
| 550 |
-
if (heightMeters > 300 &&
|
| 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
|
| 671 |
for (let id in otherPlayers) {
|
| 672 |
const op = otherPlayers[id];
|
| 673 |
if (!op.isDead && isVisible(op.y, 50)) {
|
| 674 |
-
// LERP:
|
| 675 |
-
op.x += (op.
|
| 676 |
-
op.y += (op.
|
| 677 |
-
|
| 678 |
-
player.draw(ctx, false, op.skin, op.x, op.y);
|
| 679 |
|
| 680 |
-
|
| 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 |
-
|
| 707 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|