2025-12-15 22:24:14 +07:00

1394 lines
31 KiB
JavaScript

"use strict";
const DEBUG_HITBOX = false;
const bgmList = [
{ normal: "music/Scary.mp3", gameover: "music/ScaryGO.mp3" },
{ normal: "music/Fear.mp3", gameover: "music/FearGO.mp3" },
{ normal: "music/Chill.mp3", gameover: "music/ChillGO.mp3" },
];
let currentBGM = new Audio();
let gameOverBGM = new Audio();
currentBGM.loop = true;
gameOverBGM.loop = true;
function pickRandomBGM() {
const bgm = bgmList[Math.floor(Math.random() * bgmList.length)];
currentBGM.src = bgm.normal;
gameOverBGM.src = bgm.gameover;
}
// --- OPTIMASI 1: Resolusi Tetap (HD 720p) ---
var canvasWidth = 1280;
var canvasHeight = 720;
var c, ctx;
var gameStarted = false;
var musicMuted = false;
// --- OPTIMASI 2: Cache Vignette ---
let vignetteCanvas = null;
let lastFrameTime = 0;
const frameInterval = 1000 / 60;
let cameraY = 0;
let respawnCounter = 0;
let damageFlash = 0;
let currentWave = null;
let waveCooldown = 0;
let abilityCharges = 0;
let missileAmmo = 0;
var game = {
level: 1,
speed: 1,
gameOver: false,
frames: 0,
timer: 0,
};
var keys = {
up: false,
down: false,
left: false,
right: false,
fire: false,
};
// --- LOAD IMAGES ---
var playerShipImg = new Image();
playerShipImg.src = "img/Player/pesawat22.png";
// *** GAMBAR BARU UNTUK PICKUP MISSILE ***
// Pastikan file gambar "missile_pickup.png" ada di folder img Anda
var missilePickupImg = new Image();
missilePickupImg.src = "img/Skills/missile.png";
var bg0 = new Image();
bg0.src = "img/bg_0.png";
var bg1 = new Image();
bg1.src = "img/bg_1.png";
var bg2 = new Image();
bg2.src = "img/bg_2.png";
var enemyImgArray = [];
enemyImgArray.length = 4;
let audioStarted = false;
window.addEventListener("keydown", () => {
if (!audioStarted) {
currentBGM.play().catch(() => {});
audioStarted = true;
}
});
window.addEventListener("click", () => {
if (!audioStarted) {
currentBGM.play().catch(() => {});
audioStarted = true;
}
});
for (var i = 0; i < enemyImgArray.length; i++) {
enemyImgArray[i] = new Image();
enemyImgArray[i].src = "img/alien_" + [i] + ".png";
}
var missilesArray = [];
var playerMissilesArray = [];
var enemyShipArray = [];
var enemyBulletsArray = [];
var explosions = [];
var abilityTokens = [];
var particles = [];
var laser = document.createElement("audio");
laser.src = "music/laser2.mp3";
var explosion_enemy = document.createElement("audio");
explosion_enemy.src = "music/explosion-small.mp3";
var planetImages = [];
for (let i = 1; i <= 4; i++) {
let img = new Image();
img.src = `img/SpritesPlanet/planet_${i}.png`;
planetImages.push(img);
}
let currentPlanet = null;
window.onload = function () {
init();
};
function init() {
c = document.getElementById("canvas");
ctx = c.getContext("2d", { alpha: false });
c.width = canvasWidth;
c.height = canvasHeight;
document.addEventListener("keydown", keyDownPressed, false);
document.addEventListener("keyup", keyUpPressed, false);
document.addEventListener("contextmenu", (event) => event.preventDefault());
gameStarted = true;
pickRandomBGM();
currentBGM.volume = 1;
requestAnimationFrame(gameLoop);
}
function gameLoop(timestamp) {
if (!gameStarted) return;
if (game.gameOver) {
clearGame();
drawGameOver();
return;
}
if (timestamp - lastFrameTime >= frameInterval) {
lastFrameTime = timestamp;
if (!gamePaused) {
clearGame();
updateGame();
drawGame();
} else {
drawPauseOverlay();
}
}
requestAnimationFrame(gameLoop);
}
let gamePaused = false;
function keyDownPressed(e) {
if (e.keyCode === 87 || e.keyCode === 38) keys.up = true;
else if (e.keyCode === 83 || e.keyCode === 40) keys.down = true;
if (e.keyCode === 65 || e.keyCode === 37) keys.left = true;
if (e.keyCode === 68 || e.keyCode === 39) keys.right = true;
if (e.keyCode === 32) {
keys.fire = true;
if (!player1.dead) {
fireBullet();
}
}
if (e.keyCode === 80) togglePause();
if (e.keyCode === 16) {
if (abilityCharges > 0 && !game.gameOver && !gamePaused && !player1.dead) {
useAbility();
abilityCharges--;
}
}
if (e.keyCode === 81) {
// Q
if (!game.gameOver && !gamePaused && !player1.dead) {
firePlayerMissile();
}
}
}
function keyUpPressed(e) {
if (e.keyCode === 87 || e.keyCode === 38) keys.up = false;
else if (e.keyCode === 83 || e.keyCode === 40) keys.down = false;
if (e.keyCode === 65 || e.keyCode === 37) keys.left = false;
if (e.keyCode === 68 || e.keyCode === 39) keys.right = false;
if (e.keyCode === 32) keys.fire = false;
}
function fireBullet() {
if (player1.doubleLaserTimer > 0) {
missilesArray.push(
new LaserBullet(
player1.x + player1.width,
player1.y + player1.height / 2 - 15
)
);
missilesArray.push(
new LaserBullet(
player1.x + player1.width,
player1.y + player1.height / 2 + 15
)
);
} else {
missilesArray.push(
new LaserBullet(player1.x + player1.width, player1.y + player1.height / 2)
);
}
laser.currentTime = 0;
laser.volume = 0.4;
laser.play();
createParticles(
player1.x + player1.width,
player1.y + player1.height / 2,
5,
"#00e1ff"
);
}
function firePlayerMissile() {
if (missileAmmo > 0) {
missileAmmo--;
playerMissilesArray.push(
new PlayerMissile(
player1.x + player1.width,
player1.y + player1.height / 2 - 10
)
);
let sfx = explosion_enemy.cloneNode();
sfx.volume = 0.5;
sfx.playbackRate = 2.0;
sfx.play();
}
}
function clearGame() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
}
function updateGame() {
game.frames++;
game.level = 1 + Math.floor(player1.score / 500);
game.speed = 1 + game.level * 0.1;
updateStarField();
addShips();
maybeSpawnAbilityToken();
if (keys.fire && !player1.dead && game.frames % 8 === 0) {
fireBullet();
}
if (!player1.dead) {
player1.update();
if (player1.invincible > 0) player1.invincible--;
updateCamera();
} else {
if (respawnCounter > 0) {
respawnCounter--;
if (respawnCounter <= 0 && player1.lives > 0) {
player1.dead = false;
player1.invincible = 120;
player1.x = 100;
player1.y = canvasHeight / 2 - player1.height / 2;
player1.vx = 0;
player1.vy = 0;
player1.doubleLaserTimer = 0;
}
}
}
spawnPlanet();
if (currentPlanet) currentPlanet.update();
updateParticles();
}
function drawGame() {
ctx.save();
ctx.translate(0, cameraY);
drawStarField();
if (currentPlanet) currentPlanet.draw();
drawParticles();
for (let i = 0; i < abilityTokens.length; i++) {
const t = abilityTokens[i];
t.draw();
t.update();
if (
!player1.dead &&
Tabrakan(player1.getHitbox(), {
x: t.x,
y: t.y,
width: t.width,
height: t.height,
})
) {
if (t.type === "bomb") {
abilityCharges++;
createParticles(t.x, t.y, 15, "#ffff00");
} else if (t.type === "double") {
player1.doubleLaserTimer = 600;
createParticles(t.x, t.y, 15, "#ff0000");
} else if (t.type === "missile") {
missileAmmo += 3;
createParticles(t.x, t.y, 15, "#0000ff");
}
abilityTokens.splice(i, 1);
i--;
continue;
}
if (t.x + t.width < 0) {
abilityTokens.splice(i, 1);
i--;
}
}
if (!player1.dead) {
player1.draw();
if (DEBUG_HITBOX) drawDebugHitbox(player1.getHitbox(), "lime");
}
for (let i = 0; i < enemyShipArray.length; i++) {
let s = enemyShipArray[i];
s.draw();
s.update();
if (DEBUG_HITBOX) drawDebugHitbox(s.getHitbox(), "red");
if (s.x < -200) {
enemyShipArray.splice(i, 1);
i--;
continue;
}
let shootChance = 0.005 + game.level * 0.0012;
if (shootChance > 0.04) shootChance = 0.04;
if (!player1.dead && Math.random() < shootChance && s.x > player1.x + 50) {
const ex = s.x;
const ey = s.y + s.height / 2;
const px = player1.x + player1.width / 2;
const py = player1.y + player1.height / 2;
enemyBulletsArray.push(new EnemyBullet(ex, ey, px, py));
}
if (!player1.dead && Tabrakan(player1.getHitbox(), s.getHitbox())) {
explosions.push(new Explosion(s.x + s.width / 2, s.y + s.height / 2));
createParticles(s.x + s.width / 2, s.y + s.height / 2, 20, "#ff6600");
enemyShipArray.splice(i, 1);
i--;
handlePlayerHit();
continue;
}
}
for (let i = 0; i < missilesArray.length; i++) {
let m = missilesArray[i];
m.draw();
m.update();
if (DEBUG_HITBOX) drawDebugHitbox(m.getHitbox(), "cyan");
let hit = false;
for (let j = 0; j < enemyShipArray.length; j++) {
let en = enemyShipArray[j];
if (Tabrakan(m.getHitbox(), en.getHitbox())) {
let playerDamage = 100 + game.level * 5;
en.health -= playerDamage;
createParticles(
en.x + en.width / 2,
en.y + en.height / 2,
5,
"#ff9900"
);
missilesArray.splice(i, 1);
hit = true;
if (en.health <= 0) {
player1.score += 100 + game.level * 10;
explosion_enemy.currentTime = 0;
explosion_enemy.play();
explosions.push(
new Explosion(en.x + en.width / 2, en.y + en.height / 2)
);
enemyShipArray.splice(j, 1);
}
break;
}
}
if (hit) {
i--;
continue;
}
if (m.x > canvasWidth + 50) {
missilesArray.splice(i, 1);
i--;
}
}
for (let i = 0; i < playerMissilesArray.length; i++) {
let pm = playerMissilesArray[i];
pm.draw();
pm.update();
let hit = false;
for (let j = 0; j < enemyShipArray.length; j++) {
let en = enemyShipArray[j];
if (Tabrakan(pm.getHitbox(), en.getHitbox())) {
let missileDmg = 400 + game.level * 20;
en.health -= missileDmg;
createParticles(pm.x + pm.width, pm.y, 20, "#0000ff");
explosions.push(new Explosion(pm.x + pm.width, pm.y, 0.5));
playerMissilesArray.splice(i, 1);
hit = true;
if (en.health <= 0) {
player1.score += 100 + game.level * 10;
explosion_enemy.currentTime = 0;
explosion_enemy.play();
explosions.push(
new Explosion(en.x + en.width / 2, en.y + en.height / 2)
);
enemyShipArray.splice(j, 1);
}
break;
}
}
if (hit) {
i--;
continue;
}
if (pm.x > canvasWidth + 200 || pm.y < -200 || pm.y > canvasHeight + 200) {
playerMissilesArray.splice(i, 1);
i--;
}
}
for (let i = 0; i < enemyBulletsArray.length; i++) {
let b = enemyBulletsArray[i];
b.draw();
b.update();
if (DEBUG_HITBOX) drawDebugHitbox(b.getHitbox(), "orange");
if (!player1.dead && Tabrakan(b.getHitbox(), player1.getHitbox())) {
explosions.push(
new Explosion(
player1.x + player1.width / 2,
player1.y + player1.height / 2
)
);
createParticles(
player1.x + player1.width / 2,
player1.y + player1.height / 2,
12,
"#ff3300"
);
enemyBulletsArray.splice(i, 1);
i--;
handlePlayerHit();
continue;
}
if (
b.x + b.width < -100 ||
b.x > canvasWidth + 100 ||
b.y + b.height < -100 ||
b.y > canvasHeight + 100
) {
enemyBulletsArray.splice(i, 1);
i--;
}
}
for (let i = 0; i < explosions.length; i++) {
let ex = explosions[i];
ex.draw();
ex.update();
if (ex.done) {
explosions.splice(i, 1);
i--;
}
}
ctx.restore();
drawScreenShading();
drawUI();
}
function drawDebugHitbox(rect, color) {
ctx.save();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
ctx.restore();
}
function drawUI() {
drawNewText(
"Score: " + player1.score,
canvasWidth - 200,
canvasHeight - 50,
"white"
);
drawNewText("LVL " + game.level, canvasWidth - 150, 50, "#00ff00");
let livesText = "Lives: ";
for (let i = 0; i < player1.lives; i++) {
livesText += "♥ ";
}
drawNewText(livesText, 30, canvasHeight - 50, "#ff3366");
drawNewText("Bombs (Shift): " + abilityCharges, 30, 50, "#ffff00");
drawNewText("Missiles (Q): " + missileAmmo, 30, 85, "#00ccff");
}
class PlayerObject {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 100;
this.height = 64;
this.image = playerShipImg;
this.vx = 0;
this.vy = 0;
this.acceleration = 0.8;
this.friction = 0.92;
this.maxSpeed = 10;
this.lives = 3;
this.score = 0;
this.health = 100;
this.invincible = 0;
this.dead = false;
this.doubleLaserTimer = 0;
this.totalFrames = 5;
this.frameIndex = 2;
this.spriteWidth = 0;
this.sourceHeight = 0;
this.scale = 1.0;
this.image.onload = () => {
this.spriteWidth = this.image.width / this.totalFrames;
this.sourceHeight = this.image.height;
this.width = this.spriteWidth * this.scale;
this.height = this.sourceHeight * this.scale;
this.y = canvasHeight / 2 - this.height / 2;
};
}
getHitbox() {
const h = this.height * 0.05;
const w = this.width * 0.8;
const x = this.x + (this.width - w) / 2;
const y = this.y + (this.height - h) / 2;
return { x, y, width: w, height: h };
}
draw() {
if (this.invincible > 0 && game.frames % 10 < 5) {
return;
}
ctx.save();
if (this.spriteWidth > 0) {
ctx.drawImage(
this.image,
this.frameIndex * this.spriteWidth,
0,
this.spriteWidth,
this.sourceHeight,
this.x,
this.y,
this.width,
this.height
);
} else {
ctx.fillStyle = "red";
ctx.fillRect(this.x, this.y, 50, 50);
}
ctx.restore();
}
update() {
if (keys.up) this.vy -= this.acceleration;
if (keys.down) this.vy += this.acceleration;
if (keys.left) this.vx -= this.acceleration;
if (keys.right) this.vx += this.acceleration;
this.vx *= this.friction;
this.vy *= this.friction;
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
if (speed > this.maxSpeed) {
const scale = this.maxSpeed / speed;
this.vx *= scale;
this.vy *= scale;
}
this.x += this.vx;
this.y += this.vy;
const bleedY = this.height * 0.4;
const bleedX = this.width * 0.4;
if (this.y < -bleedY) {
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.vy > 0) this.vy = 0;
}
if (this.x < -bleedX) {
this.x = -bleedX;
if (this.vx < 0) this.vx = 0;
}
if (this.x > canvasWidth - this.width + bleedX) {
this.x = canvasWidth - this.width + bleedX;
if (this.vx > 0) this.vx = 0;
}
if (this.vy < -2.5) {
this.frameIndex = 4;
} else if (this.vy < -0.5) {
this.frameIndex = 3;
} else if (this.vy > 2.5) {
this.frameIndex = 0;
} else if (this.vy > 0.5) {
this.frameIndex = 1;
} else {
this.frameIndex = 2;
}
if (this.doubleLaserTimer > 0) {
this.doubleLaserTimer--;
}
}
}
let player1 = new PlayerObject(100, 300);
function handlePlayerHit() {
if (player1.invincible > 0 || player1.dead || game.gameOver) return;
explosion_enemy.currentTime = 0;
explosion_enemy.play();
damageFlash = 20;
player1.lives--;
if (player1.lives <= 0) {
game.gameOver = true;
crossfadeToGameOver();
return;
}
player1.dead = true;
respawnCounter = 80 * 3;
}
function drawNewText(txt, x, y, color) {
ctx.font = "20px Arial";
ctx.fillStyle = color;
ctx.textAlign = "left";
ctx.fillText(txt, x, y);
}
class backgroundObj {
constructor(img, x, y, speed) {
this.x = x;
this.y = y;
this.width = 2000;
this.height = 1200;
this.img = img;
this.speed = speed;
}
draw() {
ctx.save();
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
ctx.restore();
}
update() {
this.x -= this.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);
function updateStarField() {
background3.update();
background3a.update();
background2.update();
background2a.update();
background1.update();
background1a.update();
}
function drawStarField() {
background3.draw();
background3a.draw();
background2.draw();
background2a.draw();
background1.draw();
background1a.draw();
}
function updateCamera() {
const offset = player1.y + player1.height / 2 - canvasHeight / 2;
const target = -offset * 0.7;
const bgHeight = 1200;
const minY = canvasHeight - bgHeight;
const maxY = 0;
const clamped = Math.max(minY, Math.min(maxY, target));
cameraY += (clamped - cameraY) * 0.1;
}
class LaserBullet {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 13;
this.height = 4;
this.speed = 16;
}
getHitbox() {
return { x: this.x, y: this.y, width: this.width, height: this.height };
}
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;
}
update() {
this.x += this.speed;
}
}
class PlayerMissile {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 30;
this.height = 12;
this.speed = 2;
this.maxSpeed = 18;
this.vx = 2;
this.vy = 0;
this.target = null;
}
getHitbox() {
return { x: this.x, y: this.y, width: this.width, height: this.height };
}
draw() {
ctx.save();
let g = ctx.createLinearGradient(
this.x,
this.y,
this.x + this.width,
this.y
);
g.addColorStop(0, "#00008b");
g.addColorStop(0.5, "#4169e1");
g.addColorStop(1, "#ffffff");
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();
if (Math.random() < 0.5) {
createParticles(this.x, this.y + this.height / 2, 2, "#00bfff");
}
ctx.restore();
}
update() {
this.speed *= 1.08;
if (this.speed > this.maxSpeed) this.speed = this.maxSpeed;
if (!this.target || !enemyShipArray.includes(this.target)) {
let minDist = 100000;
let closest = null;
for (let e of enemyShipArray) {
let dx = e.x - this.x;
let dy = e.y - this.y;
let d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
closest = e;
}
}
this.target = closest;
}
if (this.target) {
let tx = this.target.x + this.target.width / 2;
let ty = this.target.y + this.target.height / 2;
let dx = tx - this.x;
let dy = ty - this.y;
let angle = Math.atan2(dy, dx);
this.vx = Math.cos(angle) * this.speed;
this.vy = Math.sin(angle) * this.speed;
} else {
this.vx = this.speed;
this.vy = 0;
}
this.x += this.vx;
this.y += this.vy;
}
}
class EnemyObj {
constructor(x, y, speed, img, pattern = "straight") {
this.x = x;
this.y = y;
this.width = 145;
this.height = 90;
this.image = img;
this.speed = speed;
this.health = 100;
this.damage = 10;
this.pattern = pattern;
this.angle = 0;
}
getHitbox() {
const w = this.width * 0.55;
const h = this.height * 0.55;
const x = this.x + (this.width - w) / 2;
const y = this.y + (this.height - h) / 2;
return { x, y, width: w, height: h };
}
draw() {
ctx.save();
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
ctx.restore();
}
update() {
this.x -= this.speed;
if (this.pattern === "sine") {
this.angle += 0.05 * this.speed;
this.y += Math.sin(this.angle) * 3;
}
}
}
class EnemyBullet {
constructor(x, y, targetX, targetY) {
this.x = x;
this.y = y;
this.width = 10;
this.height = 4;
const dx = targetX - x;
const dy = targetY - y;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const speed = 8;
this.vx = (dx / len) * speed;
this.vy = (dy / len) * speed;
}
getHitbox() {
const padding = 1;
return {
x: this.x + padding,
y: this.y + padding,
width: this.width - padding * 2,
height: this.height - padding * 2,
};
}
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;
}
update() {
this.x += this.vx;
this.y += this.vy;
}
}
class Planet {
constructor(img) {
this.image = img;
this.width = 160;
this.height = 160;
this.x = canvasWidth + 50;
this.y = Math.random() * 300 + 50;
this.speed = 1.2;
this.active = true;
}
draw() {
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
update() {
this.x -= this.speed;
if (this.x < -this.width) {
this.active = false;
}
}
}
function spawnPlanet() {
if (currentPlanet == null || currentPlanet.active === false) {
let randomImg =
planetImages[Math.floor(Math.random() * planetImages.length)];
currentPlanet = new Planet(randomImg);
}
}
function addShips() {
if (game.frames < 200) return;
if (currentWave) {
if (currentWave.spawned < currentWave.count) {
if (currentWave.spawnTimer <= 0) {
spawnEnemyFromWave(currentWave);
currentWave.spawned++;
// --- RANDOM SPACING ---
let randomSpacing =
currentWave.spacing + Math.floor(Math.random() * 30);
currentWave.spawnTimer = randomSpacing;
} else {
currentWave.spawnTimer--;
}
} else {
if (enemyShipArray.length === 0) {
currentWave = null;
waveCooldown = Math.max(60, 120 - game.level * 2);
}
}
} else {
if (waveCooldown > 0) {
waveCooldown--;
} else {
startNewWave();
}
}
}
function startNewWave() {
// --- CHAOS WAVE MODE ---
let baseCount = 3;
let scalingCount = Math.floor(game.level / 2);
let count = Math.min(
20, // Max musuh ditingkatkan sedikit untuk kompensasi sebaran
baseCount + scalingCount + Math.floor(Math.random() * 5)
);
// Spacing dasar (nanti diacak lagi per musuh)
let spacing = Math.max(15, 40 - game.level);
currentWave = {
count: count,
spacing: spacing,
spawned: 0,
spawnTimer: 0,
};
}
function spawnEnemyFromWave(wave) {
// --- POSISI BENAR-BENAR ACAK ---
// Tentukan Y sembarang di area layar
// Area aman: 60px dari atas, 100px dari bawah
const minY = 60;
const maxY = canvasHeight - 120;
const y = Math.random() * (maxY - minY) + minY;
// Random X Offset biar tidak muncul dalam satu garis lurus sempurna
const xOffset = Math.random() * 200;
const randomShip = Math.floor(Math.random() * enemyImgArray.length);
// Scaling Speed per 5 Level (0.2 factor)
let rawSpeed = 3.5 + Math.random() * 2 + game.level * 0.2;
const speed = Math.min(rawSpeed, 8);
// --- RANDOM MOVEMENT TYPE ---
// Setiap musuh melempar dadu sendiri untuk menentukan tipe gerakannya
// 30% kemungkinan gerak gelombang (sine), sisanya lurus.
let movementType = Math.random() < 0.3 ? "sine" : "straight";
let enemy = new EnemyObj(
canvasWidth + 50 + xOffset, // X position + random offset
y,
speed,
enemyImgArray[randomShip],
movementType
);
// BALANCING HEALTH
enemy.health = 60 + game.level * 10;
enemyShipArray.push(enemy);
}
// --- CLASS ORB UPDATE: SUPPORT IMAGE & OUTLINE ---
class AbilityToken {
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 40; // Ukuran sedikit diperbesar untuk gambar
this.height = 40;
this.speed = 4;
// RAND: 0-0.33=Bomb, 0.33-0.66=Double, 0.66-1.0=Missile
let r = Math.random();
if (r < 0.33) this.type = "bomb";
else if (r < 0.66) this.type = "double";
else this.type = "missile";
}
draw() {
ctx.save();
if (this.type === "missile") {
// --- METAL SLUG STYLE OUTLINE (KOTAK PUTIH TEBAL) ---
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 3;
ctx.strokeRect(this.x, this.y, this.width, this.height);
// Gambar Roket Piksel di dalamnya dengan sedikit padding
const padding = 4;
ctx.drawImage(
missilePickupImg,
this.x + padding,
this.y + padding,
this.width - padding * 2,
this.height - padding * 2
);
} else {
// --- GAYA LAMA (ORB LINGKARAN) UNTUK BOMB & DOUBLE ---
ctx.beginPath();
const cx = this.x + this.width / 2;
const cy = this.y + this.height / 2;
ctx.arc(cx, cy, 16, 0, Math.PI * 2); // Radius disesuaikan dengan ukuran baru
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, 16);
g.addColorStop(0, "#ffffff");
if (this.type === "bomb") {
g.addColorStop(0.5, "#ffff00"); // Kuning
g.addColorStop(1, "#ff9900");
} else if (this.type === "double") {
g.addColorStop(0.5, "#ff3333"); // Merah
g.addColorStop(1, "#990000");
}
ctx.fillStyle = g;
ctx.fill();
}
ctx.restore();
}
update() {
this.x -= this.speed;
}
}
function maybeSpawnAbilityToken() {
// --- 0.2% (0.002) ---
if (Math.random() < 0.002 && abilityTokens.length < 3) {
const y = Math.random() * (canvasHeight - 120) + 60;
abilityTokens.push(new AbilityToken(canvasWidth + 40, y));
}
}
function useAbility() {
if (enemyShipArray.length === 0 && enemyBulletsArray.length === 0) return;
explosion_enemy.currentTime = 0;
explosion_enemy.play();
enemyShipArray.forEach((e) => {
explosions.push(new Explosion(e.x + e.width / 2, e.y + e.height / 2));
createParticles(e.x + e.width / 2, e.y + e.height / 2, 20, "#ff9900");
});
enemyShipArray = [];
enemyBulletsArray = [];
missilesArray = [];
damageFlash = 10;
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 8;
this.vy = (Math.random() - 0.5) * 8;
this.life = 30;
this.maxLife = 30;
this.color = color;
this.size = Math.random() * 3 + 2;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vx *= 0.95;
this.vy *= 0.95;
this.life--;
}
draw() {
const alpha = this.life / this.maxLife;
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
get isDead() {
return this.life <= 0;
}
}
function createParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
particles.push(new Particle(x, y, color));
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].isDead) {
particles.splice(i, 1);
}
}
}
function drawParticles() {
particles.forEach((p) => p.draw());
}
function Tabrakan(o, p) {
if (
o.x + o.width > p.x &&
o.x < p.x + p.width &&
o.y + o.height > p.y &&
o.y < p.y + p.height
) {
return true;
}
return false;
}
class Explosion {
constructor(x, y, scale = 1) {
this.x = x;
this.y = y;
this.frame = 0;
this.maxFrames = 30;
this.scale = scale;
}
update() {
this.frame++;
}
draw() {
let progress = this.frame / this.maxFrames;
let radius = (20 + 60 * progress) * this.scale;
ctx.save();
ctx.globalAlpha = 1 - progress;
let gradient = ctx.createRadialGradient(
this.x,
this.y,
0,
this.x,
this.y,
radius
);
gradient.addColorStop(0, "#ffffff");
gradient.addColorStop(0.2, "#ffe066");
gradient.addColorStop(0.5, "#ff8c42");
gradient.addColorStop(1, "#ff0000");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(this.x, this.y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
get done() {
return this.frame >= this.maxFrames;
}
}
function drawGameOver() {
ctx.fillStyle = "rgba(53, 0, 0, 0.7)";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.font = "80px Arial";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.fillText("GAME OVER", canvasWidth / 2, canvasHeight / 2 - 50);
ctx.font = "40px Arial";
ctx.fillStyle = "white";
ctx.fillText(
"Final Score: " + player1.score,
canvasWidth / 2,
canvasHeight / 2 + 20
);
ctx.fillText("Refresh to Restart", canvasWidth / 2, canvasHeight / 2 + 70);
ctx.textAlign = "left";
}
function drawPauseOverlay() {
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.font = "60px Arial";
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.fillText("PAUSED", canvasWidth / 2, canvasHeight / 2);
ctx.font = "24px Arial";
ctx.fillText("Press P to Resume", canvasWidth / 2, canvasHeight / 2 + 50);
ctx.textAlign = "left";
}
function drawScreenShading() {
if (!vignetteCanvas) {
vignetteCanvas = document.createElement("canvas");
vignetteCanvas.width = canvasWidth;
vignetteCanvas.height = canvasHeight;
const vCtx = vignetteCanvas.getContext("2d");
let grd = vCtx.createRadialGradient(
canvasWidth / 2,
canvasHeight / 2,
200,
canvasWidth / 2,
canvasHeight / 2,
canvasWidth
);
grd.addColorStop(0, "rgba(0,0,0,0)");
grd.addColorStop(1, "rgba(0,0,0,0.6)");
vCtx.fillStyle = grd;
vCtx.fillRect(0, 0, canvasWidth, canvasHeight);
}
ctx.drawImage(vignetteCanvas, 0, 0);
if (damageFlash > 0) {
let alpha = (damageFlash / 20) * 0.6;
ctx.fillStyle = "rgba(255,0,0," + alpha.toFixed(2) + ")";
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
damageFlash--;
}
}
function togglePause() {
if (game.gameOver || !gameStarted) return;
gamePaused = !gamePaused;
if (gamePaused) {
currentBGM.pause();
} else if (!musicMuted && audioStarted) {
currentBGM.play().catch(() => {});
lastFrameTime = performance.now ? performance.now() : Date.now();
}
}
function crossfadeToGameOver() {
let fadeSpeed = 0.02;
gameOverBGM.volume = 0;
gameOverBGM.play();
let fadeInterval = setInterval(() => {
currentBGM.volume -= fadeSpeed;
if (currentBGM.volume < 0) currentBGM.volume = 0;
gameOverBGM.volume += fadeSpeed;
if (gameOverBGM.volume > 1) gameOverBGM.volume = 1;
if (currentBGM.volume === 0) {
currentBGM.pause();
clearInterval(fadeInterval);
}
}, 1000 / 30);
}