diff --git a/js/game.js b/js/game.js index 59d96db..dfd8d56 100644 --- a/js/game.js +++ b/js/game.js @@ -317,6 +317,7 @@ function playSfx(type) { ptero_spawn: { f0: 520, f1: 460, dur: 0.1, wave: 'sawtooth', vol: 0.05 }, meteor_spawn: { f0: 300, f1: 200, dur: 0.14, wave: 'sawtooth', vol: 0.06 }, meteor_land: { f0: 140, f1: 70, dur: 0.18, wave: 'square', vol: 0.08 }, + portal_fade: { f0: 240, f1: 480, dur: 0.28, wave: 'triangle', vol: 0.05 }, hit: { f0: 260, f1: 140, dur: 0.1, wave: 'square', vol: 0.06 } }; @@ -409,7 +410,10 @@ function createInitialState(config = gameConfig) { worldCleared: false, portal: { active: false, + visible: false, entered: false, + fadeAlpha: 0, + fadeInSfxPlayed: false, // Keep portal close to the world edge so forward motion naturally carries // the player into it once progression is unlocked. x: worldWidth - 60, @@ -721,7 +725,7 @@ function drawMeteors() { } function drawPortal() { - if (!state.portal.active) return; + if (!state.portal.visible || state.portal.fadeAlpha <= 0.01) return; const portal = state.portal; const sx = portal.x - state.cameraX; @@ -732,7 +736,10 @@ function drawPortal() { const glowHeight = portal.height + 16; ctx.save(); - ctx.globalAlpha = 0.2 + pulse * 0.2; + ctx.globalAlpha = portal.fadeAlpha; + + ctx.save(); + ctx.globalAlpha *= 0.2 + pulse * 0.2; ctx.fillStyle = '#84c7ff'; ctx.fillRect(sx - 10, portal.y - 8, glowWidth, glowHeight); ctx.restore(); @@ -756,6 +763,7 @@ function drawPortal() { ctx.fillStyle = '#0f0f0f'; ctx.font = 'bold 14px Arial'; ctx.fillText('PORTAL', sx - 8, portal.y - 10); + ctx.restore(); } function resetWorldForDifficulty(nextDifficulty) { @@ -775,11 +783,14 @@ function resetWorldForDifficulty(nextDifficulty) { } function updatePortal(dt) { - state.portal.pulse += dt; + const portal = state.portal; + portal.pulse += dt; const nextDifficulty = getNextDifficulty(gameConfig.difficulty); if (!nextDifficulty) { - state.portal.active = false; + portal.active = false; + portal.visible = false; + portal.fadeAlpha = Math.max(0, portal.fadeAlpha - dt * 3.2); return; } @@ -787,26 +798,40 @@ function updatePortal(dt) { const worldEndX = worldWidth - p.width - 8; // Activate slightly before the hard clamp at world end so the player can // continue moving right and enter the portal without reversing direction. - const portalActivationX = Math.max(0, state.portal.x - p.width - 24); - const reachedProgressionPoint = p.x >= portalActivationX; + const portalActivationX = Math.max(0, portal.x - p.width - 24); + // Reveal portal much earlier, while keeping its fixed world position. + const portalRevealX = Math.max(0, portal.x - canvas.width * 1.6); - if (reachedProgressionPoint) { - state.portal.active = true; + portal.visible = p.x >= portalRevealX; + portal.active = p.x >= portalActivationX; + + if (portal.visible) { + portal.fadeAlpha = Math.min(1, portal.fadeAlpha + dt * 2.8); + if (!portal.fadeInSfxPlayed) { + playSfx('portal_fade'); + portal.fadeInSfxPlayed = true; + } + } else { + portal.fadeAlpha = Math.max(0, portal.fadeAlpha - dt * 3.2); + portal.fadeInSfxPlayed = false; + } + + if (portal.active) { state.worldCleared = p.x >= worldEndX; } - if (!state.portal.active || state.portal.entered) return; + if (!portal.active || portal.entered || portal.fadeAlpha < 0.9) return; const portalHitbox = { - x: state.portal.x, - y: state.portal.y, - width: state.portal.width, - height: state.portal.height + x: portal.x, + y: portal.y, + width: portal.width, + height: portal.height }; if (intersects(p, portalHitbox)) { - state.portal.entered = true; - addFloatingText(state.portal.x + state.portal.width / 2, state.portal.y - 14, `${nextDifficulty.toUpperCase()}!`, '#9fe6ff'); + portal.entered = true; + addFloatingText(portal.x + portal.width / 2, portal.y - 14, `${nextDifficulty.toUpperCase()}!`, '#9fe6ff'); resetWorldForDifficulty(nextDifficulty); } }