diff --git a/Main.html b/Main.html index eb97206..f0cfc34 100644 --- a/Main.html +++ b/Main.html @@ -1,16 +1,20 @@ + Space Game + - -
- -
- + +
+ +
+ + - + + \ No newline at end of file diff --git a/Script.js b/Script.js index 682b7bc..8ba69e0 100644 --- a/Script.js +++ b/Script.js @@ -19,14 +19,13 @@ function pickRandomBGM() { gameOverBGM.src = bgm.gameover; } -// --- OPTIMASI 1: Resolusi Tetap (HD 720p) --- var canvasWidth = 1280; var canvasHeight = 720; +var worldHeight = 900; var c, ctx; var gameStarted = false; var musicMuted = false; -// --- OPTIMASI 2: Cache Vignette --- let vignetteCanvas = null; let lastFrameTime = 0; @@ -49,6 +48,10 @@ var game = { gameOver: false, frames: 0, timer: 0, + surge: 1.0, + surgePhase: 0, + surgeTimer: 0, + surgeCooldown: 2400, // Start with 40s cooldown }; var keys = { @@ -75,6 +78,9 @@ var bombPickupImg = new Image(); // Pastikan file gambar bom clay Anda disimpan di sini bombPickupImg.src = "img/Skills/bomb.png"; +var livesImg = new Image(); +livesImg.src = "img/Player/lives.png"; + var bg0 = new Image(); bg0.src = "img/bg_0.png"; var bg1 = new Image(); @@ -89,14 +95,14 @@ let audioStarted = false; window.addEventListener("keydown", () => { if (!audioStarted) { - currentBGM.play().catch(() => {}); + currentBGM.play().catch(() => { }); audioStarted = true; } }); window.addEventListener("click", () => { if (!audioStarted) { - currentBGM.play().catch(() => {}); + currentBGM.play().catch(() => { }); audioStarted = true; } }); @@ -129,11 +135,78 @@ for (let i = 1; i <= 4; i++) { let currentPlanet = null; +let laserSprite, enemyBulletSprite, missileSprite; + +function preRenderAssets() { + // 1. CACHE LASER BULLET + // Padding for shadowBlur (15px) + const lPad = 20; + const lW = 13 + lPad * 2; + const lH = 4 + lPad * 2; + laserSprite = document.createElement("canvas"); + laserSprite.width = lW; + laserSprite.height = lH; + const lCtx = laserSprite.getContext("2d"); + + let lg = lCtx.createLinearGradient(lPad, lPad, lPad + 13, lPad); + lg.addColorStop(0, "#00e1ff"); + lg.addColorStop(0.5, "#ffffff"); + lg.addColorStop(1, "#00e1ff"); + + lCtx.fillStyle = lg; + lCtx.shadowColor = "#00ffff"; + lCtx.shadowBlur = 15; + lCtx.fillRect(lPad, lPad, 13, 4); + + + // 2. CACHE ENEMY BULLET + const ePad = 15; + const eW = 10 + ePad * 2; + const eH = 4 + ePad * 2; + enemyBulletSprite = document.createElement("canvas"); + enemyBulletSprite.width = eW; + enemyBulletSprite.height = eH; + const eCtx = enemyBulletSprite.getContext("2d"); + + let eg = eCtx.createLinearGradient(ePad + 10, ePad, ePad, ePad); // Right to Left + eg.addColorStop(0, "#ff9900"); + eg.addColorStop(0.5, "#ffffff"); + eg.addColorStop(1, "#ff3300"); + + eCtx.fillStyle = eg; + eCtx.shadowColor = "#ff6600"; + eCtx.shadowBlur = 10; + eCtx.fillRect(ePad, ePad, 10, 4); + + + // 3. CACHE PLAYER MISSILE (Body Only) + const mPad = 5; + const mW = 30 + mPad * 2; + const mH = 12 + mPad * 2; + missileSprite = document.createElement("canvas"); + missileSprite.width = mW; + missileSprite.height = mH; + const mCtx = missileSprite.getContext("2d"); + + let mg = mCtx.createLinearGradient(mPad, mPad, mPad + 30, mPad); + mg.addColorStop(0, "#00008b"); + mg.addColorStop(0.5, "#4169e1"); + mg.addColorStop(1, "#ffffff"); + + mCtx.fillStyle = mg; + mCtx.beginPath(); + mCtx.moveTo(mPad, mPad); + mCtx.lineTo(mPad + 30, mPad + 6); // height/2 + mCtx.lineTo(mPad, mPad + 12); // height + mCtx.fill(); +} + window.onload = function () { init(); }; function init() { + preRenderAssets(); // Generate sprites before game starts c = document.getElementById("canvas"); ctx = c.getContext("2d", { alpha: false }); @@ -238,7 +311,8 @@ function fireBullet() { } laser.currentTime = 0; - laser.volume = 0.4; + // IMPORTANT!! EXPERIMENT WITH THIS VALUE + laser.volume = 0.2; laser.play(); createParticles( player1.x + player1.width, @@ -272,8 +346,12 @@ function clearGame() { function updateGame() { game.frames++; + updateSurge(); + + // Base speed + Surge game.level = 1 + Math.floor(player1.score / 500); - game.speed = 1 + game.level * 0.1; + let baseSpeed = 1 + game.level * 0.1; + game.speed = baseSpeed * game.surge; updateStarField(); @@ -311,6 +389,9 @@ function updateGame() { function drawGame() { ctx.save(); + + + ctx.translate(0, cameraY); drawStarField(); @@ -480,7 +561,7 @@ function drawGame() { continue; } - if (pm.x > canvasWidth + 200 || pm.y < -200 || pm.y > canvasHeight + 200) { + if (pm.x > canvasWidth + 200 || pm.y < -200 || pm.y > worldHeight + 200) { playerMissilesArray.splice(i, 1); i--; } @@ -516,7 +597,7 @@ function drawGame() { b.x + b.width < -100 || b.x > canvasWidth + 100 || b.y + b.height < -100 || - b.y > canvasHeight + 100 + b.y > worldHeight + 100 ) { enemyBulletsArray.splice(i, 1); i--; @@ -549,21 +630,49 @@ function drawDebugHitbox(rect, color) { function drawUI() { drawNewText( - "Score: " + player1.score, - canvasWidth - 200, + player1.score, + canvasWidth - 140, canvasHeight - 50, - "white" + "white", + "30px" ); - drawNewText("LVL " + game.level, canvasWidth - 150, 50, "#00ff00"); + drawNewText(game.level, canvasWidth - 140, 50, "#00ff00", "30px"); - let livesText = "Lives: "; - for (let i = 0; i < player1.lives; i++) { - livesText += "♥ "; + // Lives (Stacked Icons) + const lifeSize = 40; // Scaled down from 104px source + const lifePadding = 5; + + if (livesImg.complete) { + for (let i = 0; i < player1.lives; i++) { + ctx.drawImage(livesImg, 30 + i * (lifeSize + lifePadding), canvasHeight - 60, lifeSize, lifeSize); + } + } else { + // Fallback if image not loaded + let livesText = "Lives: "; + for (let i = 0; i < player1.lives; i++) { + livesText += "♥ "; + } + drawNewText(livesText, 30, canvasHeight - 50, "#ff3366"); } - drawNewText(livesText, 30, canvasHeight - 50, "#ff3366"); - drawNewText("Bombs (Shift): " + abilityCharges, 30, 50, "#ffff00"); - drawNewText("Missiles (Q): " + missileAmmo, 30, 85, "#00ccff"); + // Bombs (Shift) + const iconSize = 32; + const padding = 10; + + if (bombPickupImg.complete) { + ctx.drawImage(bombPickupImg, 30, 30, iconSize, iconSize); + drawNewText("x " + abilityCharges, 30 + iconSize + padding, 30 + 24, "#ffff00"); + } else { + drawNewText("Bombs: " + abilityCharges, 30, 50, "#ffff00"); + } + + // Missiles (Q) + if (missilePickupImg.complete) { + ctx.drawImage(missilePickupImg, 30, 70, iconSize, iconSize); + drawNewText("x " + missileAmmo, 30 + iconSize + padding, 70 + 24, "#00ccff"); + } else { + drawNewText("Missiles: " + missileAmmo, 30, 85, "#00ccff"); + } } class PlayerObject { @@ -580,7 +689,7 @@ class PlayerObject { this.friction = 0.92; this.maxSpeed = 10; - this.lives = 3; + this.lives = 6; this.score = 0; this.health = 100; this.invincible = 0; @@ -666,8 +775,8 @@ class PlayerObject { this.y = -bleedY; if (this.vy < 0) this.vy = 0; } - if (this.y > canvasHeight - this.height + bleedY) { - this.y = canvasHeight - this.height + bleedY; + if (this.y > worldHeight - this.height + bleedY) { + this.y = worldHeight - this.height + bleedY; if (this.vy > 0) this.vy = 0; } @@ -719,8 +828,8 @@ function handlePlayerHit() { respawnCounter = 80 * 3; } -function drawNewText(txt, x, y, color) { - ctx.font = "20px Arial"; +function drawNewText(txt, x, y, color, fontSize = "20px") { + ctx.font = fontSize + " 'Orbitron', sans-serif"; ctx.fillStyle = color; ctx.textAlign = "left"; ctx.fillText(txt, x, y); @@ -731,9 +840,10 @@ class backgroundObj { this.x = x; this.y = y; this.width = 2000; - this.height = 1200; + this.height = 900; this.img = img; - this.speed = speed; + this.img = img; + this.factor = speed; // Rename speed to factor for parallax } draw() { ctx.save(); @@ -742,19 +852,19 @@ class backgroundObj { } update() { - this.x -= this.speed; + this.x -= this.factor * game.speed; if (this.x < -2000) { this.x = 2000; } } } -let background1 = new backgroundObj(bg0, 0, 0, game.speed * 3); -let background1a = new backgroundObj(bg0, 2000, 0, game.speed * 3); -let background2 = new backgroundObj(bg1, 0, 0, game.speed * 2); -let background2a = new backgroundObj(bg1, 2000, 0, game.speed * 2); -let background3 = new backgroundObj(bg2, 0, 0, game.speed * 1); -let background3a = new backgroundObj(bg2, 2000, 0, game.speed * 1); +let background1 = new backgroundObj(bg0, 0, 0, 3); +let background1a = new backgroundObj(bg0, 2000, 0, 3); +let background2 = new backgroundObj(bg1, 0, 0, 2); +let background2a = new backgroundObj(bg1, 2000, 0, 2); +let background3 = new backgroundObj(bg2, 0, 0, 1); +let background3a = new backgroundObj(bg2, 2000, 0, 1); function updateStarField() { background3.update(); @@ -777,7 +887,7 @@ function drawStarField() { function updateCamera() { const offset = player1.y + player1.height / 2 - canvasHeight / 2; const target = -offset * 0.7; - const bgHeight = 1200; + const bgHeight = worldHeight; const minY = canvasHeight - bgHeight; const maxY = 0; @@ -799,20 +909,8 @@ class LaserBullet { } draw() { - let g = ctx.createLinearGradient( - this.x, - this.y, - this.x + this.width, - this.y - ); - g.addColorStop(0, "#00e1ff"); - g.addColorStop(0.5, "#ffffff"); - g.addColorStop(1, "#00e1ff"); - ctx.fillStyle = g; - ctx.shadowColor = "#00ffff"; - ctx.shadowBlur = 15; - ctx.fillRect(this.x, this.y, this.width, this.height); - ctx.shadowBlur = 0; + const padding = 20; + ctx.drawImage(laserSprite, this.x - padding, this.y - padding); } update() { @@ -839,23 +937,10 @@ class PlayerMissile { draw() { ctx.save(); - // WARNA BIRU - let g = ctx.createLinearGradient( - this.x, - this.y, - this.x + this.width, - this.y - ); - g.addColorStop(0, "#00008b"); // Dark Blue - g.addColorStop(0.5, "#4169e1"); // Royal Blue - g.addColorStop(1, "#ffffff"); // White nose - ctx.fillStyle = g; - ctx.beginPath(); - ctx.moveTo(this.x, this.y); - ctx.lineTo(this.x + this.width, this.y + this.height / 2); - ctx.lineTo(this.x, this.y + this.height); - ctx.fill(); + // Draw Cached Missile Body + const padding = 5; + ctx.drawImage(missileSprite, this.x - padding, this.y - padding); // TRAIL BIRU LANGIT if (Math.random() < 0.5) { @@ -949,7 +1034,8 @@ class EnemyBullet { const dx = targetX - x; const dy = targetY - y; const len = Math.sqrt(dx * dx + dy * dy) || 1; - const speed = 8; + // bullet speedd - mark + const speed = 5; this.vx = (dx / len) * speed; this.vy = (dy / len) * speed; @@ -966,20 +1052,8 @@ class EnemyBullet { } draw() { - let g = ctx.createLinearGradient( - this.x + this.width, - this.y, - this.x, - this.y - ); - g.addColorStop(0, "#ff9900"); - g.addColorStop(0.5, "#ffffff"); - g.addColorStop(1, "#ff3300"); - ctx.fillStyle = g; - ctx.shadowColor = "#ff6600"; - ctx.shadowBlur = 10; - ctx.fillRect(this.x, this.y, this.width, this.height); - ctx.shadowBlur = 0; + const padding = 15; + ctx.drawImage(enemyBulletSprite, this.x - padding, this.y - padding); } update() { @@ -1004,7 +1078,7 @@ class Planet { } update() { - this.x -= this.speed; + this.x -= this.speed * game.surge; if (this.x < -this.width) { this.active = false; } @@ -1360,6 +1434,72 @@ function drawScreenShading() { ctx.fillRect(0, 0, canvasWidth, canvasHeight); damageFlash--; } + + // --- BRIGHTNESS SURGE OVERLAY --- + // Use 'hard-light' or 'screen' to make dark backgrounds brighter + if (game.surge > 1.0) { + let intensity = (game.surge - 1.0) / (7.0 - 1.0); // Normalized 0 to 1 + if (intensity > 0) { + ctx.save(); + ctx.globalCompositeOperation = "hard-light"; // Better for space haze + ctx.fillStyle = "white"; + // Intensity 0.0 to 1.0 -> Alpha 0.0 to 0.4 + ctx.globalAlpha = intensity * 0.4; + ctx.fillRect(0, 0, canvasWidth, canvasHeight); + ctx.restore(); + } + } +} + +function updateSurge() { + const RAMP_UP_FRAMES = 240; // 4 seconds + const HOLD_FRAMES = 780; // 13 seconds + const RAMP_DOWN_FRAMES = 300;// 5 seconds + const MAX_SURGE_SPEED = 7.0; + + // Phase 0: Cooldown + if (game.surgePhase === 0) { + if (game.surgeCooldown > 0) { + game.surgeCooldown--; + } else { + // Cooldown finished, check probability + // "2/10 chance on incidents in 40 seconds" -> 20% chance every 40s check + if (Math.random() < 0.2) { + game.surgePhase = 1; // Start Surge + } else { + game.surgeCooldown = 2400; // Wait another 40s + } + } + } + + // Phase 1: Ramping Up (Ease In) + else if (game.surgePhase === 1) { + let step = (MAX_SURGE_SPEED - 1.0) / RAMP_UP_FRAMES; + game.surge += step; + // Safety Clamp + if (game.surge >= MAX_SURGE_SPEED) { + game.surge = MAX_SURGE_SPEED; + game.surgePhase = 2; + game.surgeTimer = HOLD_FRAMES; + } + } + + // Phase 2: Holding Speed + else if (game.surgePhase === 2) { + game.surgeTimer--; + if (game.surgeTimer <= 0) game.surgePhase = 3; + } + + // Phase 3: Ramping Down (Ease Out) + else if (game.surgePhase === 3) { + let step = (MAX_SURGE_SPEED - 1.0) / RAMP_DOWN_FRAMES; + game.surge -= step; + if (game.surge <= 1.0) { + game.surge = 1.0; + game.surgePhase = 0; + game.surgeCooldown = 2400; // Start cooldown for next cycle + } + } } function togglePause() { @@ -1369,7 +1509,7 @@ function togglePause() { if (gamePaused) { currentBGM.pause(); } else if (!musicMuted && audioStarted) { - currentBGM.play().catch(() => {}); + currentBGM.play().catch(() => { }); lastFrameTime = performance.now ? performance.now() : Date.now(); } } diff --git a/img/Player/lives.png b/img/Player/lives.png new file mode 100644 index 0000000..aadaa59 Binary files /dev/null and b/img/Player/lives.png differ