up
This commit is contained in:
parent
163a7c39ff
commit
e71b5a73bb
16
Main.html
16
Main.html
@ -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
300
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();
|
||||
}
|
||||
}
|
||||
|
||||
BIN
img/Player/lives.png
Normal file
BIN
img/Player/lives.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Loading…
x
Reference in New Issue
Block a user