MASSIVE UPDATE (OVER 130+ CHANGES)

This commit is contained in:
Stanley 2025-12-20 15:34:12 +07:00
parent 435ffd6546
commit 0e4daade56
62 changed files with 1759 additions and 2468 deletions

235
3d-menu-loader.js Normal file
View File

@ -0,0 +1,235 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// Global state for pause/resume
let isMenuActive = true;
let animationFrameId = null;
let isLaunching = false;
let launchSpeed = 0;
let startPivotRef = null;
function init3DMenu() {
console.log("Initializing 3D Menu...");
const container = document.getElementById('modelContainer');
if (!container) {
console.error('modelContainer element not found in DOM');
return;
}
// --- Loading UI ---
const loadingDiv = document.createElement('div');
loadingDiv.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #00eaff;
font-family: 'Orbitron', sans-serif;
font-size: 20px;
text-align: center;
z-index: 100;
text-shadow: 0 0 10px #00eaff;
pointer-events: none;
`;
loadingDiv.innerHTML = 'INITIALIZING SYSTEM...<br><span style="font-size: 14px;">0%</span>';
container.appendChild(loadingDiv);
container.style.position = 'relative';
// --- Scene Setup ---
const scene = new THREE.Scene();
// Create a pivot group to handle the spinning independently of the model's orientation
const startPivot = new THREE.Group();
scene.add(startPivot);
startPivotRef = startPivot; // Store reference for launch animation
// Camera
const aspect = container.clientWidth / container.clientHeight;
const camera = new THREE.PerspectiveCamera(45, aspect, 0.1, 1000);
camera.position.set(0, 0, 7);
// Renderer
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(renderer.domElement);
// --- Lighting ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
const blueLight = new THREE.PointLight(0x00eaff, 3, 20);
blueLight.position.set(-2, 2, 2);
scene.add(blueLight);
// --- Model Loading ---
const loader = new GLTFLoader();
console.log("Attempting to load: img/3D.glb");
loader.load(
'img/3D.glb',
(gltf) => {
console.log("Model loaded successfully!");
const model = gltf.scene;
loadingDiv.remove();
// Auto-Scaling
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
if (maxDim > 0) {
const scale = 3.8 / maxDim;
model.scale.set(scale, scale, scale);
}
// Re-centering
const newBox = new THREE.Box3().setFromObject(model);
const center = newBox.getCenter(new THREE.Vector3());
model.position.sub(center);
// ORIENTATION CORRECTION
model.rotation.x = -Math.PI / 2;
model.rotation.z = Math.PI;
// Add the oriented model to the pivot group
startPivot.add(model);
},
(xhr) => {
if (xhr.lengthComputable) {
const percent = Math.round((xhr.loaded / xhr.total) * 100);
const span = loadingDiv.querySelector('span');
if (span) span.textContent = percent + '%';
}
},
(error) => {
console.error('Error loading model:', error);
loadingDiv.innerHTML = 'ERROR<br><span style="font-size:12px; color:red">Check Console (F12)</span>';
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true });
const cube = new THREE.Mesh(geometry, material);
startPivot.add(cube);
}
);
// --- Animation Loop ---
function animate() {
if (!isMenuActive && !isLaunching) {
animationFrameId = null;
return;
}
animationFrameId = requestAnimationFrame(animate);
if (isLaunching) {
// LAUNCH MODE: Fly upward at accelerating speed
launchSpeed += 0.015; // Acceleration
startPivot.position.y += launchSpeed;
// Straighten the ship during launch (reduce banking)
startPivot.rotation.y *= 0.95;
startPivot.rotation.z *= 0.95;
// Check if ship is out of view (Y > 15 is well off screen)
if (startPivot.position.y > 15) {
console.log("Ship has left the screen!");
isLaunching = false;
isMenuActive = false;
// Trigger callback if set
if (window._onLaunchComplete) {
window._onLaunchComplete();
}
return;
}
} else {
// NORMAL MODE: Oscillate (Roll/Bank) Left and Right
startPivot.rotation.y = Math.sin(Date.now() * 0.0015) * (Math.PI / 6);
// Gentle float with UPWARD offset (0.5)
startPivot.position.y = 0.5 + (Math.sin(Date.now() * 0.001) * 0.1);
}
renderer.render(scene, camera);
}
window._menu3DAnimate = animate;
animate();
// --- Resize Handler ---
window.addEventListener('resize', () => {
if (!container || (!isMenuActive && !isLaunching)) return;
const newAspect = container.clientWidth / container.clientHeight;
camera.aspect = newAspect;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
}
// === GLOBAL PAUSE/RESUME FUNCTIONS ===
window.pauseMenu3D = function () {
isMenuActive = false;
console.log("3D Menu rendering PAUSED");
const mainTitle = document.querySelector('.main-title');
const bgTiles = document.querySelectorAll('.bg-tile');
if (mainTitle) {
mainTitle.style.animationPlayState = 'paused';
}
bgTiles.forEach(tile => {
tile.style.animationPlayState = 'paused';
});
};
window.resumeMenu3D = function () {
isMenuActive = true;
isLaunching = false;
launchSpeed = 0;
// Reset ship position
if (startPivotRef) {
startPivotRef.position.y = 0.5;
startPivotRef.rotation.y = 0;
}
console.log("3D Menu rendering RESUMED");
const mainTitle = document.querySelector('.main-title');
const bgTiles = document.querySelectorAll('.bg-tile');
if (mainTitle) {
mainTitle.style.animationPlayState = 'running';
}
bgTiles.forEach(tile => {
tile.style.animationPlayState = 'running';
});
if (animationFrameId === null && window._menu3DAnimate) {
window._menu3DAnimate();
}
};
// === LAUNCH SHIP ANIMATION ===
// Call this to make the ship fly off screen, then execute callback
window.launchShip = function (onComplete) {
console.log("LAUNCHING SHIP!");
isLaunching = true;
launchSpeed = 0.05; // Initial speed
window._onLaunchComplete = onComplete;
// Ensure animation loop is running
if (animationFrameId === null && window._menu3DAnimate) {
window._menu3DAnimate();
}
};
// Start
init3DMenu();

View File

@ -1,326 +1,362 @@
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap');
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: 'Orbitron', sans-serif; font-family: 'Orbitron', sans-serif;
background: #0a0a0a; background: #0a0a0a;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} cursor: url('img/Custom cursor/Arrow.cur'), auto;
}
.stars { a,
position: fixed; button,
width: 100%; input[type="submit"],
height: 100%; input[type="button"],
background: radial-gradient(2px 2px at 20% 30%, white, transparent), .toggle-link,
radial-gradient(2px 2px at 60% 70%, white, transparent), .btn-send,
radial-gradient(1px 1px at 50% 50%, white, transparent), .btn-reset,
radial-gradient(1px 1px at 80% 10%, white, transparent), .toggle-btn,
radial-gradient(2px 2px at 90% 60%, white, transparent); .back-btn {
background-size: 200% 200%; cursor: url('img/Custom cursor/Stone.cur'), pointer;
animation: twinkle 8s ease-in-out infinite; }
opacity: 0.5;
}
@keyframes twinkle { .stars {
0%, 100% { opacity: 0.5; } position: fixed;
50% { opacity: 0.8; } width: 100%;
} height: 100%;
background: radial-gradient(2px 2px at 20% 30%, white, transparent),
radial-gradient(2px 2px at 60% 70%, white, transparent),
radial-gradient(1px 1px at 50% 50%, white, transparent),
radial-gradient(1px 1px at 80% 10%, white, transparent),
radial-gradient(2px 2px at 90% 60%, white, transparent);
background-size: 200% 200%;
animation: twinkle 8s ease-in-out infinite;
opacity: 0.5;
}
.neon-grid { @keyframes twinkle {
position: fixed;
width: 100%;
height: 100%;
background-image: linear-gradient(90deg, rgba(0, 255, 255, 0.15) 1px, transparent 1px);
background-size: 50px 50px;
}
.layout { 0%,
position: relative; 100% {
display: flex; opacity: 0.5;
height: 100vh; }
width: 100vw;
z-index: 1;
}
.left-panel { 50% {
width: 55%; opacity: 0.8;
display: flex; }
flex-direction: column; }
justify-content: center;
align-items: center;
padding: 40px;
}
.title-container { .neon-grid {
text-align: center; position: fixed;
} width: 100%;
height: 100%;
background-image: linear-gradient(90deg, rgba(0, 255, 255, 0.15) 1px, transparent 1px);
background-size: 50px 50px;
}
.main-title { .layout {
font-size: clamp(55px, 10vw, 140px); position: relative;
font-weight: 900; display: flex;
line-height: 0.9; height: 100vh;
background: linear-gradient(45deg, #FF006E, #8338EC, #00F5FF); width: 100vw;
background-size: 200% 200%; z-index: 1;
-webkit-background-clip: text; }
-webkit-text-fill-color: transparent;
animation: gradientShift 3s ease infinite;
text-shadow: 0 0 80px rgba(255, 0, 110, 0.5);
filter: drop-shadow(0 0 20px rgba(131, 56, 236, 0.8));
}
@keyframes gradientShift { .left-panel {
0%, 100% { background-position: 0% 50%; } width: 55%;
50% { background-position: 100% 50%; } display: flex;
} flex-direction: column;
justify-content: center;
align-items: center;
padding: 40px;
}
.subtitle { .title-container {
font-size: clamp(14px, 2vw, 20px); text-align: center;
color: #00F5FF; }
letter-spacing: 8px;
margin-top: 20px;
opacity: 0.8;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { .main-title {
0%, 100% { opacity: 0.6; } font-size: clamp(55px, 10vw, 140px);
50% { opacity: 1; } font-weight: 900;
} line-height: 0.9;
background: linear-gradient(45deg, #FF006E, #8338EC, #00F5FF);
background-size: 200% 200%;
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradientShift 3s ease infinite;
text-shadow: 0 0 80px rgba(255, 0, 110, 0.5);
filter: drop-shadow(0 0 20px rgba(131, 56, 236, 0.8));
}
.right-panel { @keyframes gradientShift {
width: 45%;
display: flex;
justify-content: center;
align-items: center;
padding: 40px;
}
.form-box { 0%,
width: 100%; 100% {
max-width: 420px; background-position: 0% 50%;
background: rgba(20, 20, 40, 0.8); }
border-radius: 20px;
padding: 40px;
border: 2px solid rgba(255, 0, 110, 0.3);
box-shadow:
0 0 40px rgba(255, 0, 110, 0.3),
0 0 80px rgba(131, 56, 236, 0.2),
inset 0 0 60px rgba(0, 245, 255, 0.05);
backdrop-filter: blur(10px);
position: relative;
}
.form-content { 50% {
position: relative; background-position: 100% 50%;
z-index: 1; }
} }
.form-title { .subtitle {
font-size: 28px; font-size: clamp(14px, 2vw, 20px);
color: #00F5FF; color: #00F5FF;
text-align: center; letter-spacing: 8px;
margin-bottom: 30px; margin-top: 20px;
text-transform: uppercase; opacity: 0.8;
letter-spacing: 3px; animation: pulse 2s ease-in-out infinite;
text-shadow: 0 0 20px rgba(0, 245, 255, 0.8); }
}
label { @keyframes pulse {
display: block;
color: #FF006E;
font-size: 14px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 700;
}
input[type="text"], 0%,
input[type="password"] { 100% {
width: 100%; opacity: 0.6;
padding: 15px 20px; }
margin-bottom: 25px;
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(0, 245, 255, 0.3);
border-radius: 10px;
color: #00F5FF;
font-size: 16px;
font-family: 'Orbitron', sans-serif;
transition: all 0.3s ease;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
}
input[type="text"]:focus, 50% {
input[type="password"]:focus { opacity: 1;
outline: none; }
border-color: #FF006E; }
box-shadow:
0 0 20px rgba(255, 0, 110, 0.5),
inset 0 0 20px rgba(255, 0, 110, 0.1);
background: rgba(0, 0, 0, 0.7);
}
input::placeholder { .right-panel {
color: rgba(0, 245, 255, 0.4); width: 45%;
font-size: 14px; display: flex;
} justify-content: center;
align-items: center;
padding: 40px;
}
.button-group { .form-box {
display: flex; width: 100%;
flex-direction: column; max-width: 420px;
align-items: center; background: rgba(20, 20, 40, 0.8);
gap: 20px; border-radius: 20px;
margin-top: 30px; padding: 40px;
} border: 2px solid rgba(255, 0, 110, 0.3);
box-shadow:
0 0 40px rgba(255, 0, 110, 0.3),
0 0 80px rgba(131, 56, 236, 0.2),
inset 0 0 60px rgba(0, 245, 255, 0.05);
backdrop-filter: blur(10px);
position: relative;
}
button { .form-content {
width: 100%; position: relative;
max-width: 250px; z-index: 1;
padding: 15px; }
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-send { .form-title {
background: linear-gradient(45deg, #FF006E, #8338EC); font-size: 28px;
color: white; color: #00F5FF;
box-shadow: 0 0 20px rgba(255, 0, 110, 0.5); text-align: center;
} margin-bottom: 30px;
text-transform: uppercase;
letter-spacing: 3px;
text-shadow: 0 0 20px rgba(0, 245, 255, 0.8);
}
.btn-send:hover { label {
box-shadow: 0 0 40px rgba(255, 0, 110, 0.8); display: block;
transform: translateY(-2px); color: #FF006E;
} font-size: 14px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 2px;
font-weight: 700;
}
.btn-reset { input[type="text"],
background: linear-gradient(45deg, #8338EC, #00F5FF); input[type="password"] {
color: white; width: 100%;
box-shadow: 0 0 20px rgba(0, 245, 255, 0.5); padding: 15px 20px;
} margin-bottom: 25px;
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(0, 245, 255, 0.3);
border-radius: 10px;
color: #00F5FF;
font-size: 16px;
font-family: 'Orbitron', sans-serif;
transition: all 0.3s ease;
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
}
.btn-reset:hover { input[type="text"]:focus,
box-shadow: 0 0 40px rgba(0, 245, 255, 0.8); input[type="password"]:focus {
transform: translateY(-2px); outline: none;
} border-color: #FF006E;
box-shadow:
0 0 20px rgba(255, 0, 110, 0.5),
inset 0 0 20px rgba(255, 0, 110, 0.1);
background: rgba(0, 0, 0, 0.7);
}
.signup-text { input::placeholder {
color: rgba(0, 245, 255, 0.7); color: rgba(0, 245, 255, 0.4);
font-size: 13px; font-size: 14px;
text-align: center; }
letter-spacing: 1px;
}
.signup-text a { .button-group {
color: #FF006E; display: flex;
text-decoration: none; flex-direction: column;
font-weight: 700; align-items: center;
transition: all 0.3s ease; gap: 20px;
text-shadow: 0 0 10px rgba(255, 0, 110, 0.3); margin-top: 30px;
} }
.signup-text a:hover { button {
color: #00F5FF; width: 100%;
text-shadow: 0 0 15px rgba(0, 245, 255, 0.8); max-width: 250px;
} padding: 15px;
font-family: 'Orbitron', sans-serif;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 2px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
button::before { .btn-send {
content: ''; background: linear-gradient(45deg, #FF006E, #8338EC);
position: absolute; color: white;
top: 50%; box-shadow: 0 0 20px rgba(255, 0, 110, 0.5);
left: 50%; }
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
button:active::before { .btn-send:hover {
width: 300px; box-shadow: 0 0 40px rgba(255, 0, 110, 0.8);
height: 300px; transform: translateY(-2px);
} }
.user-list { .btn-reset {
position: absolute; background: linear-gradient(45deg, #8338EC, #00F5FF);
bottom: 30px; color: white;
left: 50%; box-shadow: 0 0 20px rgba(0, 245, 255, 0.5);
transform: translateX(-50%); }
width: 90%;
max-width: 500px;
max-height: 200px;
overflow-y: auto;
z-index: 10;
}
.user-card { .btn-reset:hover {
background: rgba(20, 20, 40, 0.9); box-shadow: 0 0 40px rgba(0, 245, 255, 0.8);
border: 1px solid rgba(0, 245, 255, 0.4); transform: translateY(-2px);
border-radius: 10px; }
padding: 20px 25px;
margin-bottom: 10px;
text-align: center;
box-shadow: 0 0 20px rgba(0, 245, 255, 0.3);
animation: slideIn 0.4s ease;
opacity: 1;
transition: opacity 1.5s ease;
} .signup-text {
color: rgba(0, 245, 255, 0.7);
font-size: 13px;
text-align: center;
letter-spacing: 1px;
}
.usercardfade-out { .signup-text a {
color: #FF006E;
text-decoration: none;
font-weight: 700;
transition: all 0.3s ease;
text-shadow: 0 0 10px rgba(255, 0, 110, 0.3);
}
.signup-text a:hover {
color: #00F5FF;
text-shadow: 0 0 15px rgba(0, 245, 255, 0.8);
}
button::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
button:active::before {
width: 300px;
height: 300px;
}
.user-list {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 500px;
max-height: 200px;
overflow-y: auto;
z-index: 10;
}
.user-card {
background: rgba(20, 20, 40, 0.9);
border: 1px solid rgba(0, 245, 255, 0.4);
border-radius: 10px;
padding: 20px 25px;
margin-bottom: 10px;
text-align: center;
box-shadow: 0 0 20px rgba(0, 245, 255, 0.3);
animation: slideIn 0.4s ease;
opacity: 1;
transition: opacity 1.5s ease;
}
.usercardfade-out {
opacity: 0;
}
@keyframes slideIn {
from {
opacity: 0; opacity: 0;
} transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn { .user-name {
from { color: #00F5FF;
opacity: 0; font-size: 16px;
transform: translateY(20px); font-weight: 400;
} text-shadow: 0 0 10px rgba(0, 245, 255, 0.5);
to { line-height: 1.6;
opacity: 1; }
transform: translateY(0);
}
}
.user-name { .user-name strong {
color: #00F5FF; color: #FF006E;
font-size: 16px; font-weight: 700;
font-weight: 400; }
text-shadow: 0 0 10px rgba(0, 245, 255, 0.5);
line-height: 1.6;
}
.user-name strong { .user-list::-webkit-scrollbar {
color: #FF006E; width: 8px;
font-weight: 700; }
}
.user-list::-webkit-scrollbar { .user-list::-webkit-scrollbar-track {
width: 8px; background: rgba(0, 0, 0, 0.3);
} border-radius: 10px;
}
.user-list::-webkit-scrollbar-track { .user-list::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.3); background: linear-gradient(45deg, #FF006E, #8338EC);
border-radius: 10px; border-radius: 10px;
} }
.user-list::-webkit-scrollbar-thumb {
background: linear-gradient(45deg, #FF006E, #8338EC);
border-radius: 10px;
}

194
Main.html
View File

@ -4,46 +4,54 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Space Game</title> <title>Space ODYSSEY</title>
<link rel="stylesheet" href="Style.css"> <link rel="stylesheet" href="Style.css">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module" src="3d-menu-loader.js"></script>
</head> </head>
<body> <body>
<div id="mainMenu"> <div id="mainMenu">
<div class="menu-content"> <div class="scrolling-bg-container">
<h1 class="game-title"> <div class="bg-tile"></div>
<span class="title-word">SPACE</span> <div class="bg-tile flipped"></div>
<span class="title-word">ODYSSEY</span> <div class="bg-tile"></div>
</h1> </div>
<div class="menu-buttons"> <!-- Left Panel (Reserved for future content) -->
<button class="menu-btn" id="startBtn"> <div class="menu-left-panel" id="modelContainer">
<span class="btn-icon"></span> </div>
<span class="btn-text">START GAME</span>
</button>
<button class="menu-btn" id="optionBtn"> <!-- Right Panel (Angled Menu) -->
<span class="btn-icon"></span> <div class="menu-right-panel">
<span class="btn-text">OPTIONS</span> <div class="menu-content-angled">
</button> <h1 class="main-title">SPACE<br>ODYSSEY</h1>
<button class="menu-btn" id="leaderboardBtn"> <div class="menu-buttons-angled">
<span class="btn-icon">🏆</span> <button class="menu-btn-angled" id="startBtn">
<span class="btn-text">LEADERBOARD</span> <span class="btn-text">START GAME</span>
</button> </button>
<button class="menu-btn" id="exitBtn"> <button class="menu-btn-angled" id="optionBtn">
<span class="btn-icon"></span> <span class="btn-text">OPTIONS</span>
<span class="btn-text">EXIT</span> </button>
</button>
</div>
<div class="footer-info"> <button class="menu-btn-angled" id="leaderboardBtn">
<p>WASD - Move | Space - Fire | Q - Missile | Shift - Bomb | P - Pause</p> <span class="btn-text">LEADERBOARD</span>
</div> </button>
<div class="footer-info">
<p>Stanley Timothy Gunawan - 5803025021 | Jeremy Christian - 5803025025 | Philippo Mariernest A.B - 58030250</p> <button class="menu-btn-angled" id="exitBtn">
<span class="btn-text">HOW TO PLAY</span>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -68,6 +76,14 @@
<button class="toggle-btn" data-sfx="off">DISABLE</button> <button class="toggle-btn" data-sfx="off">DISABLE</button>
</div> </div>
</div> </div>
<div class="option-item">
<label>Controls</label>
<div class="button-group">
<button class="toggle-btn active" data-control="keyboard">KEYBOARD</button>
<button class="toggle-btn" data-control="mouse">MOUSE</button>
</div>
</div>
</div> </div>
<button class="menu-btn back-btn" id="backBtn"> <button class="menu-btn back-btn" id="backBtn">
@ -80,7 +96,7 @@
<div id="leaderboardMenu" style="display: none;"> <div id="leaderboardMenu" style="display: none;">
<div class="menu-content" style="min-width: 800px;"> <div class="menu-content" style="min-width: 800px;">
<h2 class="options-title">TOP PILOTS</h2> <h2 class="options-title">TOP PILOTS</h2>
<div class="leaderboard-container"> <div class="leaderboard-container">
<table class="leaderboard-table"> <table class="leaderboard-table">
<thead> <thead>
@ -92,11 +108,13 @@
</tr> </tr>
</thead> </thead>
<tbody id="leaderboardList"> <tbody id="leaderboardList">
<tr><td colspan="4">Loading Flight Data...</td></tr> <tr>
<td colspan="4">Loading Flight Data...</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<button class="menu-btn back-btn" id="lbBackBtn"> <button class="menu-btn back-btn" id="lbBackBtn">
<span class="btn-icon"></span> <span class="btn-icon"></span>
<span class="btn-text">BACK</span> <span class="btn-text">BACK</span>
@ -104,6 +122,118 @@
</div> </div>
</div> </div>
<div id="howToPlayMenu" style="display: none;">
<div class="menu-content" style="min-width: 800px; max-width: 900px;">
<h2 class="options-title">HOW TO PLAY</h2>
<div class="howtoplay-container">
<div class="howtoplay-section">
<h3 class="section-title">CONTROLS</h3>
<div class="control-guide" id="controlGuide">
<!-- Content dynamically generated in Script.js -->
<div class="control-item">
<span class="control-key">WASD / Arrow Keys</span>
<span class="control-desc">Move Your Ship</span>
</div>
<div class="control-item">
<span class="control-key">SPACE</span>
<span class="control-desc">Fire (Keyboard Mode) / Bomb (Mouse Mode)</span>
</div>
<div class="control-item">
<span class="control-key">SHIFT</span>
<span class="control-desc">Activate Bomb Ability</span>
</div>
<div class="control-item">
<span class="control-key">Q / Right Click</span>
<span class="control-desc">Fire Missile</span>
</div>
<div class="control-item">
<span class="control-key">P</span>
<span class="control-desc">Pause Game</span>
</div>
</div>
</div>
<div class="howtoplay-section">
<h3 class="section-title">ABILITIES</h3>
<div class="ability-guide">
<div class="ability-item">
<div class="ability-header">
<img src="img/Skills/bomb.png" alt="Bomb" class="ability-icon">
<span class="ability-name">Extinction Forcefield (Bomb)</span>
</div>
<p class="ability-desc">Creates a massive energy field that destroys all enemies and cancels bullets on
screen. Collect bomb tokens to recharge.</p>
</div>
<div class="ability-item">
<div class="ability-header">
<img src="img/Skills/missile.png" alt="Missile" class="ability-icon">
<span class="ability-name">Homing Missiles</span>
</div>
<p class="ability-desc">Lock-on missiles that track and destroy enemies. Limited ammo - collect missile
pickups to replenish.</p>
</div>
<div class="ability-item">
<div class="ability-header">
<img src="img/Skills/double-missile.png" alt="Double Laser" class="ability-icon">
<span class="ability-name">Double Laser</span>
</div>
<p class="ability-desc">Temporary powerup that adds angled spread shots to your main weapon for
devastating firepower.</p>
</div>
<div class="ability-item">
<div class="ability-header">
<img src="img/Player/lives.png" alt="Extra Life" class="ability-icon">
<span class="ability-name">Extra Life</span>
</div>
<p class="ability-desc">Rare pickup that grants an additional life. Drops occasionally from MiniBoss
enemies.</p>
</div>
</div>
</div>
<div class="howtoplay-section">
<h3 class="section-title">ADRENALINE SURGE</h3>
<p class="surge-desc">
The game features a dynamic <strong>Adrenaline Surge System</strong> that intensifies gameplay:
</p>
<ul class="surge-list">
<li><strong>Surge Phase:</strong> Enemy spawn rates and bullet speed increase dramatically</li>
<li><strong>Timing:</strong> Surges occur periodically with warning indicators</li>
<li><strong>Visual Cues:</strong> Screen effects and music intensify during surge</li>
<li><strong>Reward:</strong> Surviving surges grants bonus points and rare pickups</li>
</ul>
</div>
<div class="howtoplay-section cheat-section">
<h3 class="section-title" style="color: #ff006e;">CHEAT CODES</h3>
<p class="surge-desc" style="font-style: italic; opacity: 0.8;">
For testing and experimentation, but also for you to have fun:
</p>
<div class="cheat-guide">
<div class="cheat-item">
<span class="cheat-key">1</span>
<span class="cheat-desc">Add 10 Bombs</span>
</div>
<div class="cheat-item">
<span class="cheat-key">2</span>
<span class="cheat-desc">Add 30 Missiles</span>
</div>
<div class="cheat-item">
<span class="cheat-key">3</span>
<span class="cheat-desc">Trigger Surge Event</span>
</div>
</div>
</div>
</div>
<button class="menu-btn back-btn" id="htpBackBtn">
<span class="btn-icon"></span>
<span class="btn-text">BACK</span>
</button>
</div>
</div>
<div id="gameContainer" style="display: none;"> <div id="gameContainer" style="display: none;">
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
</div> </div>

52
README.md Normal file
View File

@ -0,0 +1,52 @@
# Space Odyssey
- **Gameplay utama** Kendalikan kapal, hindari gerombolan musuh, kumpulkan token kemampuan, dan tembak misil/bom.
- **Adrenaline Surge** Jendela "surge" acak yang meningkatkan laju munculnya musuh, dan fase overdrive yang meningkatkan laju tembak.
- **Bomb Spell Card** Spellcard visual yang muncul saat aktivasi ability: Bomb
- **Polished UI** Suara hover, menu animasi, kontrol berbasis glassmorphism, overlay filmgrain, dan kanvas responsif.
## 🛠️ Installation & Running
1. **Clone Repo/ Download Zip** ke folder di komputer kamu
2. Buka `play_game.bat` (Jika ingin main dengan tanpa ribet Database) `tidak disarankan untuk membuka game melalui Main.html`
Note: *May not run smoothly on low-end devices*
---
## 🎮 Controls
| Action | Keyboard | Mouse |
|---|---|---|
| Gerak | `Arrow keys` / `WASD` | `Gerakkan kursor mouse (kapal mengikutinya)` |
| Tembak (laser) | `Space` | `Klik kiri` |
| Bom | `Q` | `Klik kanan` |
| Misil | `E` | `Klik tengah` |
| Pause | `P/ESC` | `P/ESC` |
UI **Controls** otomatis berubah ketika kamu mengganti mode input di menu Options.
---
## 🔊 Audio
- **SFX** Aktif/nonaktif lewat menu Settings (`gameSettings.sfxEnabled`).
- **Surge Warning** Memutar `music/sfx/Warning.wav`.
- **GameOver** Crossfade dari `currentBGM` ke `gameOverBGM`.
---
## 👥 Team Members
| Nama | NRP |
|---|---|
| **Stanley Timoti Gunawan** | 5803025021 |
| **Phillipo M.A.B** | 5803025025 |
| **Jeremy Christian** | 5803025027 |
---
---
*In the night full of stars, we journeyed to the unknown.*

2015
Script.js

File diff suppressed because one or more lines are too long

709
Style.css
View File

@ -9,34 +9,341 @@ body {
padding: 0; padding: 0;
background-color: #000; background-color: #000;
font-family: 'Orbitron', sans-serif; font-family: 'Orbitron', sans-serif;
cursor: default;
} }
html, body { a,
button,
input[type="submit"],
input[type="button"],
.menu-btn,
.menu-btn-angled,
.toggle-btn,
.back-btn,
input[type="range"]::-webkit-slider-thumb {
cursor: pointer;
}
html,
body {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
/* === MAIN MENU, OPTIONS, & LEADERBOARD MENU === */ /* === MAIN MENU (Angled Layout with Scrolling Background) === */
/* Saya menambahkan #leaderboardMenu disini agar posisinya sama */ #mainMenu {
#mainMenu, #optionsMenu, #leaderboardMenu {
position: absolute; position: absolute;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
top: 0; top: 0;
left: 0; left: 0;
background: rgba(0, 0, 0, 0.6); display: flex;
backdrop-filter: blur(6px); flex-direction: row;
z-index: 10;
animation: fadeIn 1s ease-out;
overflow: hidden;
}
/* Seamless Scrolling Background Container */
.scrolling-bg-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
.bg-tile {
width: 100%;
height: 100vh;
background-image: url('img/Menu.jpg');
background-size: cover;
background-position: center;
/* Scroll the tiles */
animation: scrollSeamless 2.5s linear infinite;
/* Adjusted speed */
}
/* Reverse every other tile for perfectly matching edges */
.bg-tile.flipped {
transform: scaleY(-1);
}
/*
We scroll past 2 tiles (Normal + Flipped) to get back to Normal.
So we move up by 200vh.
*/
@keyframes scrollSeamless {
0% {
transform: translateY(-200vh);
}
100% {
transform: translateY(0);
}
}
/* Left Panel - Reserved for artwork */
.menu-left-panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
/* Empty for now - user will add content later */
}
/* Right Panel - Angled menu container */
.menu-right-panel {
width: 480px;
height: 100%;
background: linear-gradient(145deg,
rgba(0, 20, 40, 0.95) 0%,
rgba(0, 40, 60, 0.9) 50%,
rgba(0, 30, 50, 0.95) 100%);
transform: skewX(-8deg);
transform-origin: top right;
display: flex;
align-items: center;
justify-content: center;
border-left: 4px solid #00eaff;
border-right: 4px solid #00eaff;
box-shadow:
-20px 0 60px rgba(0, 234, 255, 0.3),
-5px 0 20px rgba(0, 234, 255, 0.5),
inset 5px 0 30px rgba(0, 234, 255, 0.1),
20px 0 60px rgba(0, 234, 255, 0.3),
5px 0 20px rgba(0, 234, 255, 0.5),
inset -5px 0 30px rgba(0, 234, 255, 0.1);
position: relative;
}
/* Un-skew the content inside */
.menu-content-angled {
transform: skewX(8deg);
text-align: center;
padding: 50px 40px 50px 20px;
/* Adjusted padding: more right, less left */
}
/* Main Title (Login1 Style - Gradient) */
.main-title {
font-size: clamp(40px, 8vw, 70px);
font-weight: 900;
line-height: 0.9;
margin-bottom: 40px;
margin-left: 30px;
transform: translateX(20px);
/* Shift title to the right */
background: linear-gradient(45deg, #FF006E, #8338EC, #00F5FF);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 3s ease infinite;
text-shadow: 0 0 80px rgba(255, 0, 110, 0.5);
filter: drop-shadow(0 0 20px rgba(131, 56, 236, 0.8));
}
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
/* Angled Menu Buttons Container */
.menu-buttons-angled {
display: flex;
flex-direction: column;
gap: 18px;
position: relative;
}
/* Angled Menu Button Style */
.menu-btn-angled {
font-size: 20px;
padding: 18px 50px;
cursor: pointer;
background: linear-gradient(145deg,
rgba(0, 60, 80, 0.8) 0%,
rgba(0, 40, 60, 0.9) 50%,
rgba(0, 50, 70, 0.8) 100%);
color: #00eaff;
border: 2px solid #00eaff;
border-radius: 8px;
box-shadow:
0 4px 0 #007a8c,
0 0 15px rgba(0, 234, 255, 0.3),
inset 0 0 20px rgba(0, 234, 255, 0.1);
transition: all 0.25s ease;
font-family: 'Orbitron', sans-serif;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 3px;
min-width: 280px;
position: relative;
overflow: hidden;
}
.menu-btn-angled:nth-child(1) {
transform: translateX(17px);
min-width: 280px;
}
.menu-btn-angled:nth-child(2) {
transform: translateX(2px);
min-width: 300px;
}
.menu-btn-angled:nth-child(3) {
transform: translateX(-13px);
min-width: 320px;
}
.menu-btn-angled:nth-child(4) {
transform: translateX(-27px);
min-width: 340px;
}
/* Glow effect on buttons */
.menu-btn-angled::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(0, 234, 255, 0.2),
transparent);
transition: left 0.5s ease;
}
.menu-btn-angled:hover::before {
left: 100%;
}
.menu-btn-angled:nth-child(1):hover {
transform: translateX(-60px) translateY(-4px) scale(1.02);
background: linear-gradient(145deg,
rgba(0, 234, 255, 0.9) 0%,
rgba(0, 188, 212, 0.95) 50%,
rgba(0, 234, 255, 0.9) 100%);
color: #000;
box-shadow:
0 8px 0 #007a8c,
0 0 30px rgba(0, 234, 255, 0.6),
-10px 0 40px rgba(0, 234, 255, 0.4);
border-color: #ffffff;
}
.menu-btn-angled:nth-child(2):hover {
transform: translateX(-60px) translateY(-4px) scale(1.02);
background: linear-gradient(145deg,
rgba(0, 234, 255, 0.9) 0%,
rgba(0, 188, 212, 0.95) 50%,
rgba(0, 234, 255, 0.9) 100%);
color: #000;
box-shadow:
0 8px 0 #007a8c,
0 0 30px rgba(0, 234, 255, 0.6),
-10px 0 40px rgba(0, 234, 255, 0.4);
border-color: #ffffff;
}
.menu-btn-angled:nth-child(3):hover {
transform: translateX(-60px) translateY(-4px) scale(1.02);
background: linear-gradient(145deg,
rgba(0, 234, 255, 0.9) 0%,
rgba(0, 188, 212, 0.95) 50%,
rgba(0, 234, 255, 0.9) 100%);
color: #000;
box-shadow:
0 8px 0 #007a8c,
0 0 30px rgba(0, 234, 255, 0.6),
-10px 0 40px rgba(0, 234, 255, 0.4);
border-color: #ffffff;
}
.menu-btn-angled:nth-child(4):hover {
transform: translateX(-60px) translateY(-4px) scale(1.02);
background: linear-gradient(145deg,
rgba(0, 234, 255, 0.9) 0%,
rgba(0, 188, 212, 0.95) 50%,
rgba(0, 234, 255, 0.9) 100%);
color: #000;
box-shadow:
0 8px 0 #007a8c,
0 0 30px rgba(0, 234, 255, 0.6),
-10px 0 40px rgba(0, 234, 255, 0.4);
border-color: #ffffff;
}
.menu-btn-angled:nth-child(1):active {
transform: translateX(-60px) translateY(2px) scale(1.01);
box-shadow:
0 2px 0 #007a8c,
0 0 20px rgba(0, 234, 255, 0.5);
}
.menu-btn-angled:nth-child(2):active {
transform: translateX(-60px) translateY(2px) scale(1.01);
box-shadow:
0 2px 0 #007a8c,
0 0 20px rgba(0, 234, 255, 0.5);
}
.menu-btn-angled:nth-child(3):active {
transform: translateX(-60px) translateY(2px) scale(1.01);
box-shadow:
0 2px 0 #007a8c,
0 0 20px rgba(0, 234, 255, 0.5);
}
.menu-btn-angled:nth-child(4):active {
transform: translateX(-60px) translateY(2px) scale(1.01);
box-shadow:
0 2px 0 #007a8c,
0 0 20px rgba(0, 234, 255, 0.5);
}
/* OPTIONS & LEADERBOARD MENU (Keep centered) */
#optionsMenu,
#leaderboardMenu,
#howToPlayMenu {
position: absolute;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 10; z-index: 10;
animation: fadeIn 1s ease-out; animation: fadeIn 0.5s ease-out;
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
/* === STARS CONTAINER (hidden by default) === */ /* === STARS CONTAINER (hidden by default) === */
@ -83,6 +390,7 @@ html, body {
10px 10px 14px rgba(0, 234, 255, 0.9), 10px 10px 14px rgba(0, 234, 255, 0.9),
0 0 35px #00eaff; 0 0 35px #00eaff;
} }
to { to {
text-shadow: text-shadow:
2px 2px 0px #00eaff, 2px 2px 0px #00eaff,
@ -194,7 +502,7 @@ html, body {
margin-bottom: 40px; margin-bottom: 40px;
min-width: 600px; min-width: 600px;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
box-shadow: box-shadow:
0 0 20px rgba(0, 234, 255, 0.3), 0 0 20px rgba(0, 234, 255, 0.3),
inset 0 0 20px rgba(0, 234, 255, 0.1); inset 0 0 20px rgba(0, 234, 255, 0.1);
} }
@ -248,7 +556,7 @@ input[type="range"]::-webkit-slider-thumb {
background: linear-gradient(145deg, #00eaff, #00bcd4); background: linear-gradient(145deg, #00eaff, #00bcd4);
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
box-shadow: box-shadow:
0 0 10px rgba(0, 234, 255, 0.8), 0 0 10px rgba(0, 234, 255, 0.8),
0 2px 4px rgba(0, 0, 0, 0.5); 0 2px 4px rgba(0, 0, 0, 0.5);
transition: all 0.2s ease; transition: all 0.2s ease;
@ -257,7 +565,7 @@ input[type="range"]::-webkit-slider-thumb {
input[type="range"]::-webkit-slider-thumb:hover { input[type="range"]::-webkit-slider-thumb:hover {
background: #00eaff; background: #00eaff;
box-shadow: box-shadow:
0 0 20px rgba(0, 234, 255, 1), 0 0 20px rgba(0, 234, 255, 1),
0 4px 8px rgba(0, 0, 0, 0.6); 0 4px 8px rgba(0, 0, 0, 0.6);
transform: scale(1.15); transform: scale(1.15);
@ -284,7 +592,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
transition: all 0.3s ease; transition: all 0.3s ease;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 1px; letter-spacing: 1px;
box-shadow: box-shadow:
0 2px 0 rgba(0, 188, 212, 0.3), 0 2px 0 rgba(0, 188, 212, 0.3),
inset 0 0 10px rgba(0, 234, 255, 0.1); inset 0 0 10px rgba(0, 234, 255, 0.1);
} }
@ -293,7 +601,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
color: #00eaff; color: #00eaff;
border-color: #00eaff; border-color: #00eaff;
background: linear-gradient(145deg, #1b1b1b, #2a2a2a); background: linear-gradient(145deg, #1b1b1b, #2a2a2a);
box-shadow: box-shadow:
0 2px 0 #00bcd4, 0 2px 0 #00bcd4,
0 0 15px rgba(0, 234, 255, 0.4), 0 0 15px rgba(0, 234, 255, 0.4),
inset 0 0 15px rgba(0, 234, 255, 0.2); inset 0 0 15px rgba(0, 234, 255, 0.2);
@ -303,7 +611,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
color: #000; color: #000;
background: #00eaff; background: #00eaff;
border-color: #00eaff; border-color: #00eaff;
box-shadow: box-shadow:
0 4px 0 #00bcd4, 0 4px 0 #00bcd4,
0 0 20px rgba(0, 234, 255, 0.8), 0 0 20px rgba(0, 234, 255, 0.8),
inset 0 0 10px rgba(255, 255, 255, 0.3); inset 0 0 10px rgba(255, 255, 255, 0.3);
@ -316,7 +624,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
background: linear-gradient(145deg, #1b1b1b, #111); background: linear-gradient(145deg, #1b1b1b, #111);
border-color: #00eaff; border-color: #00eaff;
color: #00eaff; color: #00eaff;
box-shadow: box-shadow:
0 4px 0 #00bcd4, 0 4px 0 #00bcd4,
0 0 12px rgba(0, 234, 255, 0.4), 0 0 12px rgba(0, 234, 255, 0.4),
inset 0 0 15px rgba(0, 234, 255, 0.2); inset 0 0 15px rgba(0, 234, 255, 0.2);
@ -326,7 +634,7 @@ input[type="range"]::-webkit-slider-thumb:hover {
background: linear-gradient(145deg, #2a2a2a, #1b1b1b); background: linear-gradient(145deg, #2a2a2a, #1b1b1b);
border-color: #00eaff; border-color: #00eaff;
color: #ffffff; color: #ffffff;
box-shadow: box-shadow:
0 6px 0 #00bcd4, 0 6px 0 #00bcd4,
0 0 25px rgba(0, 234, 255, 0.6), 0 0 25px rgba(0, 234, 255, 0.6),
inset 0 0 20px rgba(0, 234, 255, 0.3); inset 0 0 20px rgba(0, 234, 255, 0.3);
@ -382,14 +690,237 @@ input[type="range"]::-webkit-slider-thumb:hover {
} }
/* Leaderboard Rank Colors */ /* Leaderboard Rank Colors */
.rank-1 td { color: #ffd700; text-shadow: 0 0 10px #ffd700; font-weight: 900; font-size: 20px; } .rank-1 td {
.rank-2 td { color: #c0c0c0; text-shadow: 0 0 10px #c0c0c0; font-weight: 700; } color: #ffd700;
.rank-3 td { color: #cd7f32; text-shadow: 0 0 10px #cd7f32; font-weight: 700; } text-shadow: 0 0 10px #ffd700;
font-weight: 900;
font-size: 20px;
}
.rank-2 td {
color: #c0c0c0;
text-shadow: 0 0 10px #c0c0c0;
font-weight: 700;
}
.rank-3 td {
color: #cd7f32;
text-shadow: 0 0 10px #cd7f32;
font-weight: 700;
}
/* Custom Scrollbar for Leaderboard */ /* Custom Scrollbar for Leaderboard */
.leaderboard-container::-webkit-scrollbar { width: 8px; } .leaderboard-container::-webkit-scrollbar {
.leaderboard-container::-webkit-scrollbar-thumb { background: #00eaff; border-radius: 4px; } width: 8px;
.leaderboard-container::-webkit-scrollbar-track { background: rgba(0,0,0,0.5); } }
.leaderboard-container::-webkit-scrollbar-thumb {
background: #00eaff;
border-radius: 4px;
}
.leaderboard-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.5);
}
/* === HOW TO PLAY MENU STYLES === */
.howtoplay-container {
background: rgba(0, 0, 0, 0.7);
border: 2px solid rgba(0, 234, 255, 0.4);
border-radius: 20px;
padding: 30px 40px;
margin-bottom: 30px;
max-height: 500px;
overflow-y: auto;
backdrop-filter: blur(10px);
box-shadow: 0 0 30px rgba(0, 234, 255, 0.15);
text-align: center;
}
.howtoplay-section {
margin-bottom: 30px;
padding-bottom: 25px;
border-bottom: 1px solid rgba(0, 234, 255, 0.2);
}
.howtoplay-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
font-size: 24px;
font-weight: 700;
color: #00eaff;
margin-bottom: 15px;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(0, 234, 255, 0.5);
font-family: 'Orbitron', sans-serif;
}
/* Control Guide Styles */
.control-guide {
display: flex;
flex-direction: column;
gap: 12px;
}
.control-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
background: rgba(0, 234, 255, 0.05);
border-left: 3px solid #00eaff;
border-radius: 5px;
transition: all 0.2s ease;
}
.control-item:hover {
background: rgba(0, 234, 255, 0.1);
transform: translateX(5px);
}
.control-key {
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 16px;
color: #00eaff;
background: rgba(0, 234, 255, 0.1);
padding: 8px 15px;
border-radius: 5px;
border: 1px solid rgba(0, 234, 255, 0.3);
min-width: 180px;
text-align: center;
}
.control-desc {
font-size: 14px;
color: #ffffff;
flex: 1;
margin-left: 20px;
}
/* Ability Guide Styles */
.ability-guide {
display: flex;
flex-direction: column;
gap: 15px;
}
.ability-item {
padding: 15px;
background: rgba(0, 234, 255, 0.05);
border-radius: 8px;
border: 1px solid rgba(0, 234, 255, 0.2);
transition: all 0.2s ease;
}
.ability-item:hover {
background: rgba(0, 234, 255, 0.08);
border-color: rgba(0, 234, 255, 0.4);
}
.ability-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.ability-icon {
width: 40px;
height: 40px;
object-fit: contain;
filter: drop-shadow(0 0 8px rgba(0, 234, 255, 0.5));
}
.ability-name {
display: inline;
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 16px;
color: #00eaff;
text-shadow: 0 0 5px rgba(0, 234, 255, 0.3);
}
.ability-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
margin: 0;
}
/* Surge Description */
.surge-desc {
font-size: 15px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.7;
margin-bottom: 15px;
text-align: center;
}
.surge-desc strong {
color: #00eaff;
text-shadow: 0 0 5px rgba(0, 234, 255, 0.3);
}
/* Lists */
.surge-list,
.tips-list {
list-style: none;
padding: 0;
margin: 0;
text-align: left;
display: inline-block;
max-width: 700px;
}
.surge-list li,
.tips-list li {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
line-height: 1.8;
margin-bottom: 10px;
padding-left: 25px;
position: relative;
}
.surge-list li::before {
content: '⚡';
position: absolute;
left: 0;
color: #00eaff;
}
.tips-list li::before {
content: '▸';
position: absolute;
left: 5px;
color: #00eaff;
font-size: 18px;
}
.surge-list li strong,
.tips-list li strong {
color: #00eaff;
}
/* Custom Scrollbar for How to Play */
.howtoplay-container::-webkit-scrollbar {
width: 8px;
}
.howtoplay-container::-webkit-scrollbar-thumb {
background: #00eaff;
border-radius: 4px;
}
.howtoplay-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.5);
}
/* === GAME CONTAINER === */ /* === GAME CONTAINER === */
#gameContainer { #gameContainer {
@ -408,12 +939,130 @@ input[type="range"]::-webkit-slider-thumb:hover {
/* === RESPONSIVE === */ /* === RESPONSIVE === */
@media (max-width: 768px) { @media (max-width: 768px) {
.title-word { font-size: 60px; } .title-word {
.menu-btn { font-size: 20px; padding: 12px 40px; min-width: 280px; } font-size: 60px;
.options-container { min-width: 90vw; padding: 30px; } }
.options-title { font-size: 50px; }
.footer-info p { font-size: 12px; } .menu-btn {
font-size: 20px;
padding: 12px 40px;
min-width: 280px;
}
.options-container {
min-width: 90vw;
padding: 30px;
}
.options-title {
font-size: 50px;
}
.footer-info p {
font-size: 12px;
}
/* Responsive Leaderboard */ /* Responsive Leaderboard */
.leaderboard-table th, .leaderboard-table td { font-size: 14px; padding: 10px; } .leaderboard-table th,
.leaderboard-table td {
font-size: 14px;
padding: 10px;
}
}
/* === FAKE LOADING SCREEN === */
#loadingOverlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: black;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10001;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.loading-dots {
display: flex;
gap: 20px;
}
.loading-dots span {
font-size: 80px;
color: #00eaff;
font-family: 'Orbitron', sans-serif;
animation: dotPulse 1.4s infinite linear;
text-shadow: 0 0 20px rgba(0, 234, 255, 0.8);
}
.loading-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dots span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes dotPulse {
0%,
100% {
opacity: 0.2;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
/* === CHEAT CODES STYLES === */
.cheat-guide {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 15px;
}
.cheat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
background: rgba(255, 0, 110, 0.05);
border-left: 3px solid #ff006e;
border-radius: 5px;
transition: all 0.2s ease;
}
.cheat-item:hover {
background: rgba(255, 0, 110, 0.1);
transform: translateX(5px);
}
.cheat-key {
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 16px;
color: #ff006e;
background: rgba(255, 0, 110, 0.1);
padding: 8px 15px;
border-radius: 5px;
border: 1px solid rgba(255, 0, 110, 0.3);
min-width: 60px;
text-align: center;
}
.cheat-desc {
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
font-weight: bold;
letter-spacing: 1px;
} }

View File

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
session_start();
header('Content-Type: application/json; charset=utf-8');
$DB_HOST = '127.0.0.1';
$DB_NAME = 'space_odyssey';
$DB_USER = 'root';
$DB_PASS = '';
function json_out(int $status, array $data): void {
http_response_code($status);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
try {
$dsn = "mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4";
$pdo = new PDO($dsn, $DB_USER, $DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (Throwable $e) {
json_out(500, ['ok' => false, 'error' => 'DB connection failed']);
}
function current_user(PDO $pdo): ?array {
if (!isset($_SESSION['user_id'])) return null;
$stmt = $pdo->prepare('SELECT id, username, email, created_at FROM users WHERE id = ? LIMIT 1');
$stmt->execute([(int)$_SESSION['user_id']]);
$u = $stmt->fetch();
return $u ?: null;
}

View File

@ -1,33 +1,28 @@
<?php <?php
declare(strict_types=1); session_start();
require __DIR__ . '/config.php'; header('Content-Type: application/json');
require_once __DIR__ . '/../database/db_connect.php';
$data = json_decode(file_get_contents('php://input') ?: '[]', true); $input = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) json_out(400, ['ok' => false, 'error' => 'Invalid JSON']); $username = trim($input['login'] ?? '');
$password = $input['password'] ?? '';
$login = isset($data['login']) ? trim((string)$data['login']) : ''; if (!$username || !$password) {
$password = isset($data['password']) ? (string)$data['password'] : ''; http_response_code(400);
echo json_encode(['error' => 'Username and password required']);
if ($login === '' || $password === '') { exit;
json_out(400, ['ok' => false, 'error' => 'Missing login or password']);
} }
$stmt = $pdo->prepare('SELECT id, username, email, password_hash, created_at FROM users WHERE username = ? OR email = ? LIMIT 1'); $stmt = $pdo->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->execute([$login, $login]); $stmt->execute([$username]);
$user = $stmt->fetch(); $user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, (string)$user['password_hash'])) { if ($user && password_verify($password, $user['password'])) {
json_out(401, ['ok' => false, 'error' => 'Invalid credentials']); $_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
echo json_encode(['ok' => true, 'user' => ['id' => $user['id'], 'username' => $user['username']]]);
} else {
http_response_code(401);
echo json_encode(['error' => 'Invalid credentials']);
} }
?>
$_SESSION['user_id'] = (int)$user['id'];
json_out(200, [
'ok' => true,
'user' => [
'id' => (int)$user['id'],
'username' => (string)$user['username'],
'email' => $user['email'],
'created_at' => $user['created_at'],
]
]);

View File

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
require __DIR__ . '/config.php';
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], (bool)$p['secure'], (bool)$p['httponly']);
}
session_destroy();
json_out(200, ['ok' => true]);

View File

@ -1,6 +1,10 @@
<?php <?php
declare(strict_types=1); session_start();
require __DIR__ . '/config.php'; header('Content-Type: application/json');
$u = current_user($pdo); if (isset($_SESSION['user_id'])) {
json_out(200, ['ok' => true, 'user' => $u]); echo json_encode(['ok' => true, 'user' => ['id' => $_SESSION['user_id'], 'username' => $_SESSION['username']]]);
} else {
echo json_encode(['ok' => false]);
}
?>

View File

@ -1,41 +1,40 @@
<?php <?php
declare(strict_types=1); session_start();
require __DIR__ . '/config.php'; header('Content-Type: application/json');
require_once __DIR__ . '/../database/db_connect.php';
$data = json_decode(file_get_contents('php://input') ?: '[]', true); $input = json_decode(file_get_contents('php://input'), true);
if (!is_array($data)) json_out(400, ['ok' => false, 'error' => 'Invalid JSON']); $username = trim($input['username'] ?? '');
$password = $input['password'] ?? '';
$username = isset($data['username']) ? trim((string)$data['username']) : ''; if (!$username || !$password) {
$email = isset($data['email']) ? trim((string)$data['email']) : ''; http_response_code(400);
$password = isset($data['password']) ? (string)$data['password'] : ''; echo json_encode(['error' => 'Username and password required']);
exit;
if ($username === '' || strlen($username) < 3 || strlen($username) > 32) {
json_out(400, ['ok' => false, 'error' => 'Username must be 3-32 chars']);
}
if (!preg_match('/^[A-Za-z0-9_]+$/', $username)) {
json_out(400, ['ok' => false, 'error' => 'Username must be letters/numbers/_ only']);
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
json_out(400, ['ok' => false, 'error' => 'Invalid email']);
}
if (strlen($password) < 6) {
json_out(400, ['ok' => false, 'error' => 'Password must be at least 6 chars']);
} }
// Check if user exists
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetch()) {
http_response_code(409);
echo json_encode(['error' => 'Username already taken']);
exit;
}
// Hash password
$hash = password_hash($password, PASSWORD_DEFAULT); $hash = password_hash($password, PASSWORD_DEFAULT);
try { // Insert user
$stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)'); $stmt = $pdo->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->execute([$username, ($email === '' ? null : $email), $hash]); if ($stmt->execute([$username, $hash])) {
$userId = $pdo->lastInsertId();
$_SESSION['user_id'] = (int)$pdo->lastInsertId(); $_SESSION['user_id'] = $userId;
$u = current_user($pdo); $_SESSION['username'] = $username;
json_out(201, ['ok' => true, 'user' => $u]); echo json_encode(['ok' => true, 'user' => ['id' => $userId, 'username' => $username]]);
} catch (Throwable $e) { } else {
$msg = $e->getMessage(); http_response_code(500);
if (stripos($msg, 'Duplicate') !== false || stripos($msg, 'uq_') !== false) { echo json_encode(['error' => 'Registration failed']);
json_out(409, ['ok' => false, 'error' => 'Username or email already used']);
}
json_out(500, ['ok' => false, 'error' => 'Register failed']);
} }
?>

51
api/score.php Normal file
View File

@ -0,0 +1,51 @@
<?php
session_start();
header('Content-Type: application/json');
require_once __DIR__ . '/../database/db_connect.php';
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
// Get Top 10 Scores
// Join with users table to get username
$sql = "SELECT users.username, scores.score, scores.level
FROM scores
JOIN users ON scores.user_id = users.id
ORDER BY scores.score DESC
LIMIT 10";
$stmt = $pdo->query($sql);
$data = [];
$rank = 1;
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$row['rank'] = $rank++;
$data[] = $row;
}
echo json_encode(['success' => true, 'data' => $data]);
} elseif ($method === 'POST') {
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Not logged in']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$score = intval($input['score'] ?? 0);
$level = intval($input['level'] ?? 1);
if ($score <= 0) {
echo json_encode(['ok' => true]);
exit;
}
$stmt = $pdo->prepare("INSERT INTO scores (user_id, score, level) VALUES (?, ?, ?)");
if ($stmt->execute([$_SESSION['user_id'], $score, $level])) {
echo json_encode(['ok' => true]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to save score']);
}
}
?>

27
database/README.md Normal file
View File

@ -0,0 +1,27 @@
# Database Setup (MySQL/XAMPP)
This project has been upgraded to use **MySQL** for shared global leaderboards and user accounts.
## Requirements
- XAMPP (or any WAMP/MAMP stack)
- PHP 7.4 or higher
- MySQL / MariaDB
## Setup Instructions
1. **Start XAMPP**: Open XAMPP Control Panel and start **Apache** and **MySQL**.
2. **Move Project**: Ensure the project folder is inside `C:\xampp\htdocs\`.
3. **Configure Database**:
- Go to `http://localhost/phpmyadmin`
- Create a new database named `space_odyssey`.
4. **Access Game**:
- Open your browser to `http://localhost/Kelompok09-SPOILER/Main.html`.
## Structure
- `/database/db_connect.php`: MySQL connection script (configured for default XAMPP).
- `/api/`: Contains the PHP endpoints for the game.
## Troubleshooting
If you see "Database connection failed", ensure:
1. MySQL is running in XAMPP.
2. The database name in `db_connect.php` matches your phpMyAdmin database name.

41
database/db_connect.php Normal file
View File

@ -0,0 +1,41 @@
<?php
$host = 'localhost';
$db = 'space_odyssey';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
$pdo->exec("CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;");
$pdo->exec("CREATE TABLE IF NOT EXISTS scores (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
score INT NOT NULL,
level INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (score),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;");
} catch (\PDOException $e) {
header('Content-Type: application/json');
echo json_encode(['error' => "Database connection failed: " . $e->getMessage()]);
exit;
}
?>

BIN
img/3D.glb Normal file

Binary file not shown.

BIN
img/Menu.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 656 KiB

BIN
img/SpellBomb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 KiB

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
img/cockpit_family.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

BIN
music/MainMenu.mp3 Normal file

Binary file not shown.

Binary file not shown.

BIN
music/audacity/LD1edit.wav Normal file

Binary file not shown.

BIN
music/audacity/LD2.wav Normal file

Binary file not shown.

BIN
music/audacity/RainH1.wav Normal file

Binary file not shown.

BIN
music/audacity/RainH2.wav Normal file

Binary file not shown.

BIN
music/audacity/Scaryfu.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
music/sfx/BombHIT.wav Normal file

Binary file not shown.

BIN
music/sfx/BombMerged.wav Normal file

Binary file not shown.

BIN
music/sfx/BossAttack.wav Normal file

Binary file not shown.

BIN
music/sfx/ClockAura.wav Normal file

Binary file not shown.

BIN
music/sfx/DEFEATED.wav Normal file

Binary file not shown.

BIN
music/sfx/SpiralPlayer.wav Normal file

Binary file not shown.

BIN
music/sfx/Warning.wav Normal file

Binary file not shown.

BIN
music/sfx/bomb-get.mp3 Normal file

Binary file not shown.

BIN
music/sfx/grazedezpz.wav Normal file

Binary file not shown.

BIN
music/sfx/laser2.mp3 Normal file

Binary file not shown.

BIN
music/sfx/lifeup.wav Normal file

Binary file not shown.

BIN
music/sfx/missile-get.mp3 Normal file

Binary file not shown.

BIN
music/sfx/missileaway.mp3 Normal file

Binary file not shown.

BIN
music/sfx/playerdeadlol.mp3 Normal file

Binary file not shown.

BIN
music/sfx/weapon-get.mp3 Normal file

Binary file not shown.

5
play_game.bat Normal file
View File

@ -0,0 +1,5 @@
start "" "http://localhost:8000/Main.html"
python -m http.server 8000
pause

126
surge-shake-system.js Normal file
View File

@ -0,0 +1,126 @@
const surgeShake = {
active: false,
startTime: 0,
duration: 0,
fadeOutStart: 0,
amplitude: 10,
frequency: 50,
offsetX: 0,
offsetY: 0,
offsetRotation: 0,
directionChangeTimer: 0,
directionChangeInterval: 2,
currentDirectionX: 0,
currentDirectionY: 0,
currentDirectionRot: 0,
blurIntensity: 0,
start() {
this.active = true;
this.startTime = Date.now();
this.duration = 14500;
this.fadeOutStart = this.duration - 1200;
this.amplitude = 10;
this.frequency = 50;
this.directionChangeTimer = 0;
console.log("SURGE SHAKE: ACTIVATED");
},
stop() {
this.active = false;
this.offsetX = 0;
this.offsetY = 0;
this.offsetRotation = 0;
this.blurIntensity = 0;
console.log("SURGE SHAKE: DEACTIVATED");
},
update() {
if (!this.active) {
this.offsetX = 0;
this.offsetY = 0;
this.offsetRotation = 0;
this.blurIntensity = 0;
return;
}
let elapsed = Date.now() - this.startTime;
if (elapsed >= this.duration) {
this.stop();
return;
}
let intensity = 1.0;
if (elapsed >= this.fadeOutStart) {
let fadeProgress = (elapsed - this.fadeOutStart) / 1200;
intensity = 1.0 - fadeProgress;
}
this.directionChangeTimer++;
if (this.directionChangeTimer >= this.directionChangeInterval) {
this.directionChangeTimer = 0;
this.currentDirectionX = (Math.random() - 0.5) * 2;
this.currentDirectionY = (Math.random() - 0.5) * 2;
this.currentDirectionRot = (Math.random() - 0.5) * 2;
}
let timeScale = elapsed * 0.001;
let oscillationX = Math.sin(timeScale * this.frequency * (1 + Math.random() * 0.2));
let oscillationY = Math.cos(timeScale * this.frequency * (1 + Math.random() * 0.2));
let oscillationRot = Math.sin(timeScale * this.frequency * 0.5) * (Math.random() * 0.4 + 0.8);
let jitter = 0.8 + Math.random() * 0.4;
let effectiveAmplitude = this.amplitude * intensity * jitter;
this.offsetX = this.currentDirectionX * oscillationX * effectiveAmplitude;
this.offsetY = this.currentDirectionY * oscillationY * effectiveAmplitude;
this.offsetRotation = this.currentDirectionRot * oscillationRot * 0.015 * intensity; // Small rotation in radians
this.blurIntensity = intensity * 8;
}
};
const surgeFlash = {
active: false,
intensity: 0,
pulseSpeed: 3,
start() {
this.active = true;
this.intensity = 0;
console.log("SURGE FLASH: ACTIVATED");
},
stop() {
this.active = false;
this.intensity = 0;
console.log("SURGE FLASH: DEACTIVATED");
},
update() {
if (!this.active) {
this.intensity = 0;
return;
}
let time = Date.now() * 0.001;
this.intensity = 0.15 + Math.sin(time * this.pulseSpeed) * 0.1;
},
apply(ctx) {
if (!this.active || this.intensity <= 0) return;
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.fillStyle = `rgba(255, 255, 255, ${this.intensity * 0.5})`;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.restore();
}
};