1848 lines
43 KiB
JavaScript
1848 lines
43 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;
|
|
}
|
|
|
|
var canvasWidth = 1280;
|
|
var canvasHeight = 720;
|
|
var worldHeight = 900;
|
|
var c, ctx;
|
|
var gameStarted = false;
|
|
var musicMuted = false;
|
|
|
|
let vignetteCanvas = null;
|
|
|
|
let lastFrameTime = 0;
|
|
const frameInterval = 1000 / 90;
|
|
|
|
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,
|
|
surge: 1.0,
|
|
surgePhase: 0,
|
|
surgeTimer: 0,
|
|
surgeCooldown: 2400,
|
|
};
|
|
|
|
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 PICKUP SKILLS ---
|
|
var missilePickupImg = new Image();
|
|
missilePickupImg.src = "img/Skills/missile.png";
|
|
|
|
var laserPickupImg = new Image();
|
|
laserPickupImg.src = "img/Skills/double-missile.png";
|
|
|
|
// *** GAMBAR BARU UNTUK PICKUP BOMB ***
|
|
var bombPickupImg = new Image();
|
|
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();
|
|
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/SpritesShips/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;
|
|
|
|
let laserSprite, enemyBulletSprite, missileSprite;
|
|
|
|
function preRenderAssets() {
|
|
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);
|
|
|
|
|
|
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);
|
|
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);
|
|
|
|
|
|
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);
|
|
mCtx.lineTo(mPad, mPad + 12);
|
|
mCtx.fill();
|
|
}
|
|
|
|
window.onload = function () {
|
|
init();
|
|
};
|
|
|
|
function init() {
|
|
preRenderAssets();
|
|
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.2;
|
|
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++;
|
|
|
|
updateSurge();
|
|
|
|
// Base speed + Surge
|
|
game.level = 1 + Math.floor(player1.score / 500);
|
|
let baseSpeed = 1 + game.level * 0.1;
|
|
game.speed = baseSpeed * game.surge;
|
|
|
|
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 > worldHeight + 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 > worldHeight + 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(
|
|
player1.score,
|
|
canvasWidth - 140,
|
|
canvasHeight - 50,
|
|
"white",
|
|
"30px"
|
|
);
|
|
drawNewText(game.level, canvasWidth - 140, 50, "#00ff00", "30px");
|
|
|
|
// Lives (Stacked Icons)
|
|
const lifeSize = 40;
|
|
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");
|
|
}
|
|
|
|
// 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 {
|
|
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 = 6;
|
|
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 > worldHeight - this.height + bleedY) {
|
|
this.y = worldHeight - 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, fontSize = "20px") {
|
|
ctx.font = fontSize + " 'Orbitron', sans-serif";
|
|
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 = 900;
|
|
this.img = img;
|
|
this.img = img;
|
|
this.factor = speed;
|
|
}
|
|
draw() {
|
|
ctx.save();
|
|
ctx.drawImage(this.img, this.x, this.y, this.width, this.height);
|
|
ctx.restore();
|
|
}
|
|
|
|
update() {
|
|
this.x -= this.factor * game.speed;
|
|
if (this.x < -2000) {
|
|
this.x = 2000;
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
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 = worldHeight;
|
|
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() {
|
|
const padding = 20;
|
|
ctx.drawImage(laserSprite, this.x - padding, this.y - padding);
|
|
}
|
|
|
|
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();
|
|
|
|
const padding = 5;
|
|
ctx.drawImage(missileSprite, this.x - padding, this.y - padding);
|
|
|
|
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;
|
|
// bullet speedd - mark
|
|
const speed = 5;
|
|
|
|
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() {
|
|
const padding = 15;
|
|
ctx.drawImage(enemyBulletSprite, this.x - padding, this.y - padding);
|
|
}
|
|
|
|
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 * game.surge;
|
|
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++;
|
|
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,
|
|
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 MUSUH ACAK ---
|
|
const minY = 60;
|
|
const maxY = canvasHeight - 120;
|
|
|
|
const y = Math.random() * (maxY - minY) + minY;
|
|
|
|
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);
|
|
|
|
|
|
let movementType = Math.random() < 0.3 ? "sine" : "straight";
|
|
|
|
let enemy = new EnemyObj(
|
|
canvasWidth + 50 + xOffset,
|
|
y,
|
|
speed,
|
|
enemyImgArray[randomShip],
|
|
movementType
|
|
);
|
|
|
|
// ENEMY 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;
|
|
this.height = 40;
|
|
this.speed = 4;
|
|
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();
|
|
|
|
ctx.strokeStyle = "#ffffff";
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeRect(this.x, this.y, this.width, this.height);
|
|
|
|
const padding = 4;
|
|
let imgToDraw = null;
|
|
|
|
if (this.type === "missile") {
|
|
imgToDraw = missilePickupImg;
|
|
} else if (this.type === "double") {
|
|
imgToDraw = laserPickupImg;
|
|
} else if (this.type === "bomb") {
|
|
imgToDraw = bombPickupImg;
|
|
}
|
|
|
|
if (imgToDraw) {
|
|
ctx.drawImage(
|
|
imgToDraw,
|
|
this.x + padding,
|
|
this.y + padding,
|
|
this.width - padding * 2,
|
|
this.height - padding * 2
|
|
);
|
|
}
|
|
|
|
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");
|
|
// --- FIX: Tambah Score ---
|
|
player1.score += 100 + game.level * 10;
|
|
});
|
|
|
|
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--;
|
|
}
|
|
|
|
// --- BRIGHTNESS SURGE OVERLAY ---
|
|
if (game.surge > 1.0) {
|
|
let intensity = (game.surge - 1.0) / (7.0 - 1.0);
|
|
if (intensity > 0) {
|
|
ctx.save();
|
|
ctx.globalCompositeOperation = "hard-light";
|
|
ctx.fillStyle = "white";
|
|
ctx.globalAlpha = intensity * 0.4;
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
ctx.restore();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateSurge() {
|
|
const RAMP_UP_FRAMES = 240;
|
|
const HOLD_FRAMES = 780;
|
|
const RAMP_DOWN_FRAMES = 300;
|
|
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() {
|
|
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);
|
|
}
|
|
|
|
// === GAME SETTINGS ===
|
|
const gameSettings = {
|
|
musicEnabled: true,
|
|
sfxEnabled: true
|
|
};
|
|
|
|
// === DOM ELEMENTS ===
|
|
const mainMenu = document.getElementById('mainMenu');
|
|
const optionsMenu = document.getElementById('optionsMenu');
|
|
const gameContainer = document.getElementById('gameContainer');
|
|
|
|
const startBtn = document.getElementById('startBtn');
|
|
const optionBtn = document.getElementById('optionBtn');
|
|
const exitBtn = document.getElementById('exitBtn');
|
|
const backBtn = document.getElementById('backBtn');
|
|
|
|
// Toggle Buttons
|
|
const musicBtns = document.querySelectorAll('[data-music]');
|
|
const sfxBtns = document.querySelectorAll('[data-sfx]');
|
|
|
|
// === MENU SOUNDS ===
|
|
const menuHoverSound = new Audio();
|
|
menuHoverSound.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=';
|
|
const menuClickSound = new Audio();
|
|
menuClickSound.src = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=';
|
|
|
|
// === PREVENT DEFAULT GAME START ===
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
gameStarted = false;
|
|
});
|
|
|
|
// === START BUTTON ===
|
|
startBtn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
|
|
// Apply settings to game
|
|
applySettings();
|
|
|
|
// Hide menu, show game
|
|
mainMenu.style.display = 'none';
|
|
gameContainer.style.display = 'block';
|
|
|
|
// Start the game
|
|
if (!gameStarted) {
|
|
init();
|
|
}
|
|
});
|
|
|
|
// === OPTION BUTTON ===
|
|
optionBtn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
mainMenu.style.display = 'none';
|
|
optionsMenu.style.display = 'flex';
|
|
});
|
|
|
|
// === EXIT BUTTON ===
|
|
exitBtn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
|
|
// Create exit confirmation
|
|
const confirmExit = document.createElement('div');
|
|
confirmExit.style.cssText = `
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: rgba(0, 0, 0, 0.9);
|
|
border: 3px solid #00eaff;
|
|
border-radius: 20px;
|
|
padding: 50px 60px;
|
|
z-index: 1000;
|
|
text-align: center;
|
|
box-shadow: 0 0 40px rgba(0, 234, 255, 0.6);
|
|
backdrop-filter: blur(10px);
|
|
`;
|
|
|
|
confirmExit.innerHTML = `
|
|
<h3 style="
|
|
color: #00eaff;
|
|
font-size: 48px;
|
|
margin-bottom: 20px;
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-weight: 900;
|
|
text-shadow:
|
|
2px 2px 0px #00bcd4,
|
|
4px 4px 0px #009bb0,
|
|
0 0 30px #00eaff;
|
|
letter-spacing: 3px;
|
|
">
|
|
EXIT GAME?
|
|
</h3>
|
|
<p style="
|
|
color: #ffffff;
|
|
margin-bottom: 40px;
|
|
font-size: 18px;
|
|
font-family: 'Orbitron', sans-serif;
|
|
opacity: 0.8;
|
|
">
|
|
Are you sure you want to leave?
|
|
</p>
|
|
<div style="display: flex; gap: 20px; justify-content: center;">
|
|
<button id="confirmYes" style="
|
|
padding: 15px 50px;
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
background: linear-gradient(145deg, #111, #1b1b1b);
|
|
color: #ff3366;
|
|
border: 2px solid #ff3366;
|
|
border-radius: 40px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
box-shadow: 0 4px 0 #cc0033, 0 0 12px rgba(255, 51, 102, 0.4), inset 0 0 15px rgba(255, 51, 102, 0.1);
|
|
">YES</button>
|
|
<button id="confirmNo" style="
|
|
padding: 15px 50px;
|
|
font-family: 'Orbitron', sans-serif;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
background: linear-gradient(145deg, #111, #1b1b1b);
|
|
color: #00eaff;
|
|
border: 2px solid #00eaff;
|
|
border-radius: 40px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
box-shadow: 0 4px 0 #00bcd4, 0 0 12px rgba(0, 234, 255, 0.4), inset 0 0 15px rgba(0, 234, 255, 0.2);
|
|
">NO</button>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(confirmExit);
|
|
|
|
// YES button hover effect
|
|
const yesBtn = document.getElementById('confirmYes');
|
|
yesBtn.addEventListener('mouseenter', () => {
|
|
yesBtn.style.background = '#ff3366';
|
|
yesBtn.style.color = '#000';
|
|
yesBtn.style.transform = 'translateY(-5px) scale(1.08)';
|
|
yesBtn.style.boxShadow = '0 10px 20px rgba(255, 51, 102, 0.6), 0 0 30px #ff3366';
|
|
});
|
|
yesBtn.addEventListener('mouseleave', () => {
|
|
yesBtn.style.background = 'linear-gradient(145deg, #111, #1b1b1b)';
|
|
yesBtn.style.color = '#ff3366';
|
|
yesBtn.style.transform = 'translateY(0) scale(1)';
|
|
yesBtn.style.boxShadow = '0 4px 0 #cc0033, 0 0 12px rgba(255, 51, 102, 0.4), inset 0 0 15px rgba(255, 51, 102, 0.1)';
|
|
});
|
|
|
|
// NO button hover effect
|
|
const noBtn = document.getElementById('confirmNo');
|
|
noBtn.addEventListener('mouseenter', () => {
|
|
noBtn.style.background = '#00eaff';
|
|
noBtn.style.color = '#000';
|
|
noBtn.style.transform = 'translateY(-5px) scale(1.08)';
|
|
noBtn.style.boxShadow = '0 10px 20px rgba(0, 234, 255, 0.6), 0 0 30px #00eaff';
|
|
});
|
|
noBtn.addEventListener('mouseleave', () => {
|
|
noBtn.style.background = 'linear-gradient(145deg, #111, #1b1b1b)';
|
|
noBtn.style.color = '#00eaff';
|
|
noBtn.style.transform = 'translateY(0) scale(1)';
|
|
noBtn.style.boxShadow = '0 4px 0 #00bcd4, 0 0 12px rgba(0, 234, 255, 0.4), inset 0 0 15px rgba(0, 234, 255, 0.2)';
|
|
});
|
|
|
|
// Click handlers
|
|
yesBtn.addEventListener('click', () => {
|
|
window.close();
|
|
setTimeout(() => {
|
|
window.location.href = 'about:blank';
|
|
}, 100);
|
|
});
|
|
|
|
noBtn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
document.body.removeChild(confirmExit);
|
|
});
|
|
});
|
|
|
|
// === BACK BUTTON ===
|
|
backBtn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
optionsMenu.style.display = 'none';
|
|
mainMenu.style.display = 'flex';
|
|
});
|
|
|
|
// === MUSIC BUTTONS ===
|
|
musicBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
|
|
musicBtns.forEach(b => b.classList.remove('active'));
|
|
|
|
btn.classList.add('active');
|
|
|
|
gameSettings.musicEnabled = btn.dataset.music === 'on';
|
|
|
|
if (typeof currentBGM !== 'undefined') {
|
|
if (gameSettings.musicEnabled) {
|
|
currentBGM.volume = 1;
|
|
if (!game.gameOver && gameStarted) {
|
|
currentBGM.play().catch(() => {});
|
|
}
|
|
} else {
|
|
currentBGM.volume = 0;
|
|
currentBGM.pause();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// === SFX BUTTONS ===
|
|
sfxBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
playSound(menuClickSound);
|
|
|
|
sfxBtns.forEach(b => b.classList.remove('active'));
|
|
|
|
btn.classList.add('active');
|
|
|
|
gameSettings.sfxEnabled = btn.dataset.sfx === 'on';
|
|
});
|
|
});
|
|
|
|
// === HOVER SOUNDS ===
|
|
document.querySelectorAll('.menu-btn, .toggle-btn').forEach(btn => {
|
|
btn.addEventListener('mouseenter', () => {
|
|
if (gameSettings.sfxEnabled) {
|
|
playSound(menuHoverSound);
|
|
}
|
|
});
|
|
});
|
|
|
|
// === APPLY SETTINGS TO GAME ===
|
|
function applySettings() {
|
|
// Apply music setting
|
|
if (typeof currentBGM !== 'undefined') {
|
|
if (gameSettings.musicEnabled) {
|
|
currentBGM.volume = 1;
|
|
} else {
|
|
currentBGM.volume = 0;
|
|
currentBGM.pause();
|
|
}
|
|
}
|
|
|
|
if (typeof gameOverBGM !== 'undefined') {
|
|
if (gameSettings.musicEnabled) {
|
|
gameOverBGM.volume = 1;
|
|
} else {
|
|
gameOverBGM.volume = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// === PLAY SOUND HELPER ===
|
|
function playSound(audio) {
|
|
if (gameSettings.sfxEnabled) {
|
|
audio.volume = 0.3;
|
|
audio.currentTime = 0;
|
|
audio.play().catch(() => {});
|
|
}
|
|
}
|
|
|
|
// === KEYBOARD NAVIGATION ===
|
|
document.addEventListener('keydown', (e) => {
|
|
// ESC to go back
|
|
if (e.key === 'Escape') {
|
|
if (optionsMenu.style.display === 'flex') {
|
|
backBtn.click();
|
|
} else if (gameContainer.style.display === 'block' && typeof togglePause !== 'undefined') {
|
|
togglePause();
|
|
}
|
|
}
|
|
|
|
// Enter to start game from main menu
|
|
if (e.key === 'Enter' && mainMenu.style.display === 'flex') {
|
|
startBtn.click();
|
|
}
|
|
});
|
|
|
|
// === MODIFY EXISTING GAME FUNCTIONS ===
|
|
|
|
window.onload = function () {
|
|
console.log('Game Ready - Waiting for menu start...');
|
|
};
|
|
|
|
// Add return to menu function
|
|
function returnToMenu() {
|
|
currentBGM.pause();
|
|
gameOverBGM.pause();
|
|
currentBGM.currentTime = 0;
|
|
gameOverBGM.currentTime = 0;
|
|
|
|
game.gameOver = false;
|
|
game.frames = 0;
|
|
game.level = 1;
|
|
game.speed = 1;
|
|
game.surge = 1.0;
|
|
game.surgePhase = 0;
|
|
game.surgeTimer = 0;
|
|
game.surgeCooldown = 2400;
|
|
|
|
missilesArray = [];
|
|
playerMissilesArray = [];
|
|
enemyShipArray = [];
|
|
enemyBulletsArray = [];
|
|
explosions = [];
|
|
abilityTokens = [];
|
|
particles = [];
|
|
|
|
player1 = new PlayerObject(100, 300);
|
|
player1.lives = 6;
|
|
player1.score = 0;
|
|
|
|
respawnCounter = 0;
|
|
damageFlash = 0;
|
|
currentWave = null;
|
|
waveCooldown = 0;
|
|
abilityCharges = 0;
|
|
missileAmmo = 0;
|
|
cameraY = 0;
|
|
gameStarted = false;
|
|
gamePaused = false;
|
|
|
|
if (gameContainer) gameContainer.style.display = 'none';
|
|
if (mainMenu) mainMenu.style.display = 'flex';
|
|
}
|
|
|
|
console.log('Menu System Loaded');
|
|
console.log('Settings:', gameSettings); |