This commit is contained in:
Stanley 2025-12-16 02:44:44 +07:00
parent 163a7c39ff
commit e71b5a73bb
3 changed files with 230 additions and 86 deletions

View File

@ -1,16 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Game</title>
<link rel="stylesheet" href="Style.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<div id="game">
<canvas id="canvas"></canvas>
</div>
<script src="Script.js"></script>
<body>
<div id="game">
<canvas id="canvas"></canvas>
</div>
<script src="Script.js"></script>
</body>
</html>
</html>

300
Script.js
View File

@ -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();
}
}

BIN
img/Player/lives.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB