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