1083 lines
46 KiB
HTML
1083 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
|
<title>星际文明探索者 | Star Civilization Explorer</title>
|
|
|
|
<!-- CDN 引入依赖库 -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.3/howler.min.js"></script>
|
|
|
|
<style>
|
|
/* --- 核心样式变量 --- */
|
|
:root {
|
|
--bg-deep: #05070a;
|
|
--primary-glow: #00f3ff;
|
|
--secondary-glow: #bd00ff;
|
|
--glass-bg: rgba(10, 15, 30, 0.65);
|
|
--glass-border: rgba(255, 255, 255, 0.1);
|
|
--text-main: #e0e6ed;
|
|
--font-stack: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; }
|
|
|
|
body {
|
|
background-color: var(--bg-deep);
|
|
color: var(--text-main);
|
|
font-family: var(--font-stack);
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
}
|
|
|
|
/* --- 画布层 --- */
|
|
#game-canvas {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
|
|
/* --- UI 层 --- */
|
|
#ui-layer {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 10;
|
|
pointer-events: none; /* 让点击穿透到 Canvas (用于飞船控制) */
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
grid-template-columns: 250px 1fr 250px;
|
|
padding: 20px;
|
|
gap: 20px;
|
|
}
|
|
|
|
/* 通用玻璃拟态面板 */
|
|
.glass-panel {
|
|
background: var(--glass-bg);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid var(--glass-border);
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
|
|
pointer-events: auto;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
}
|
|
|
|
.glass-panel::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 0; width: 100%; height: 2px;
|
|
background: linear-gradient(90deg, transparent, var(--primary-glow), transparent);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.glass-panel:hover {
|
|
box-shadow: 0 0 15px rgba(0, 243, 255, 0.2);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
/* --- HUD 布局 --- */
|
|
.hud-top-left { grid-row: 1; grid-column: 1; display: flex; flex-direction: column; gap: 10px; }
|
|
.hud-center { grid-row: 1 / 3; grid-column: 2; position: relative; display: flex; justify-content: center; pointer-events: none; }
|
|
.hud-bottom-left { grid-row: 3; grid-column: 1; }
|
|
.hud-top-right { grid-row: 1; grid-column: 3; display: flex; flex-direction: column; gap: 10px; align-items: flex-end; }
|
|
.hud-bottom-right { grid-row: 3; grid-column: 3; }
|
|
.hud-overlay { grid-row: 1 / 4; grid-column: 1 / 4; display: flex; justify-content: center; align-items: center; z-index: 100; }
|
|
|
|
/* 指标模块 */
|
|
.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; font-size: 14px; letter-spacing: 1px; text-transform: uppercase; }
|
|
.stat-label { opacity: 0.7; }
|
|
.stat-value { color: var(--primary-glow); font-weight: bold; text-shadow: 0 0 5px var(--primary-glow); }
|
|
|
|
.progress-bar { width: 100%; height: 4px; background: rgba(255,255,255,0.1); margin-top: 4px; border-radius: 2px; overflow: hidden; }
|
|
.progress-fill { height: 100%; background: var(--primary-glow); width: 0%; transition: width 0.3s; box-shadow: 0 0 8px var(--primary-glow); }
|
|
|
|
/* --- 按钮样式 --- */
|
|
.btn {
|
|
background: rgba(0, 243, 255, 0.1);
|
|
border: 1px solid var(--primary-glow);
|
|
color: var(--primary-glow);
|
|
padding: 10px 20px;
|
|
font-family: inherit;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
transition: all 0.2s;
|
|
font-size: 12px;
|
|
width: 100%;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.btn:hover { background: var(--primary-glow); color: #000; box-shadow: 0 0 15px var(--primary-glow); }
|
|
.btn:disabled { opacity: 0.3; cursor: not-allowed; border-color: #555; color: #555; background: transparent; box-shadow: none; }
|
|
|
|
.btn-upgrade { border-color: var(--secondary-glow); color: var(--secondary-glow); background: rgba(189, 0, 255, 0.1); }
|
|
.btn-upgrade:hover { background: var(--secondary-glow); color: #fff; box-shadow: 0 0 15px var(--secondary-glow); }
|
|
|
|
/* --- 启动/菜单屏幕 --- */
|
|
#start-screen {
|
|
background: rgba(5, 7, 10, 0.95);
|
|
z-index: 200;
|
|
flex-direction: column;
|
|
text-align: center;
|
|
gap: 30px;
|
|
}
|
|
|
|
.title {
|
|
font-size: 4rem;
|
|
font-weight: 900;
|
|
background: linear-gradient(135deg, #fff, var(--primary-glow), var(--secondary-glow));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
text-shadow: 0 0 30px rgba(0, 243, 255, 0.3);
|
|
letter-spacing: 4px;
|
|
}
|
|
|
|
.subtitle { font-size: 1.2rem; opacity: 0.8; color: var(--primary-glow); }
|
|
|
|
.controls-hint {
|
|
margin-top: 20px;
|
|
font-size: 0.9rem;
|
|
color: #889;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.key {
|
|
display: inline-block;
|
|
padding: 2px 6px;
|
|
border: 1px solid #555;
|
|
border-radius: 4px;
|
|
background: rgba(255,255,255,0.1);
|
|
font-family: monospace;
|
|
color: #fff;
|
|
}
|
|
|
|
/* --- 通知系统 --- */
|
|
#notifications {
|
|
position: absolute;
|
|
top: 20%;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
pointer-events: none;
|
|
width: 400px;
|
|
}
|
|
|
|
.notify-msg {
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border: 1px solid var(--primary-glow);
|
|
color: #fff;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
text-align: center;
|
|
}
|
|
|
|
/* --- 战斗提示 --- */
|
|
#combat-alert {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 3rem;
|
|
color: #ff3333;
|
|
text-shadow: 0 0 20px #ff0000;
|
|
font-weight: bold;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
letter-spacing: 5px;
|
|
border: 4px solid #ff3333;
|
|
padding: 20px 40px;
|
|
background: rgba(50, 0, 0, 0.5);
|
|
display: none;
|
|
}
|
|
|
|
/* --- 移动端适配 --- */
|
|
@media (max-width: 768px) {
|
|
.title { font-size: 2.5rem; }
|
|
#ui-layer { display: flex; flex-direction: column; padding: 10px; }
|
|
.glass-panel { margin-bottom: 5px; padding: 5px; }
|
|
.hud-center { display: none; } /* 隐藏中心瞄准镜,节省空间 */
|
|
.controls-hint { font-size: 0.8rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- 3D Canvas -->
|
|
<canvas id="game-canvas"></canvas>
|
|
|
|
<!-- UI Layer -->
|
|
<div id="ui-layer">
|
|
|
|
<!-- Left Stats -->
|
|
<div class="hud-top-left">
|
|
<div class="glass-panel">
|
|
<div class="stat-row">
|
|
<span class="stat-label">Shields</span>
|
|
<span class="stat-value" id="val-shield">100%</span>
|
|
</div>
|
|
<div class="progress-bar"><div class="progress-fill" id="bar-shield" style="width: 100%"></div></div>
|
|
</div>
|
|
<div class="glass-panel">
|
|
<div class="stat-row">
|
|
<span class="stat-label">Hull</span>
|
|
<span class="stat-value" id="val-hull" style="color: #ff5555">100%</span>
|
|
</div>
|
|
<div class="progress-bar"><div class="progress-fill" id="bar-hull" style="width: 100%; background: #ff5555; box-shadow: 0 0 8px #ff5555;"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Center (Notifications & Alert) -->
|
|
<div class="hud-center">
|
|
<div id="notifications"></div>
|
|
<div id="combat-alert">WARNING: HOSTILES</div>
|
|
</div>
|
|
|
|
<!-- Right Resources & Upgrades -->
|
|
<div class="hud-top-right">
|
|
<div class="glass-panel" style="width: 100%">
|
|
<div class="stat-row">
|
|
<span class="stat-label">Resources</span>
|
|
<span class="stat-value" id="val-res" style="color: #ffd700">0</span>
|
|
</div>
|
|
<div class="stat-row" style="font-size: 10px; opacity: 0.5;">
|
|
<span>Crystals</span>
|
|
<span id="val-crystals">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-panel" style="width: 100%; margin-top: 10px;">
|
|
<div style="padding: 10px; text-align: right; font-size: 12px; color: var(--primary-glow); letter-spacing: 2px; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 5px;">ENGINEERING</div>
|
|
<button class="btn btn-upgrade" id="btn-up-engine" onclick="game.upgrade('engine')">Engines (100)</button>
|
|
<button class="btn btn-upgrade" id="btn-up-shield" onclick="game.upgrade('shield')">Shields (150)</button>
|
|
<button class="btn btn-upgrade" id="btn-up-laser" onclick="game.upgrade('laser')">Lasers (200)</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom Status -->
|
|
<div class="hud-bottom-left glass-panel">
|
|
<div class="stat-row">
|
|
<span class="stat-label">Speed</span>
|
|
<span class="stat-value" id="val-speed">0</span>
|
|
</div>
|
|
<div class="stat-row">
|
|
<span class="stat-label">Status</span>
|
|
<span class="stat-value" id="val-status" style="color: #aaa;">Drifting</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map Controls (Simple) -->
|
|
<div class="hud-bottom-right glass-panel">
|
|
<div class="stat-row" style="justify-content: flex-end; gap: 15px;">
|
|
<span style="font-size: 10px; opacity: 0.5;">ZOOM: SCROLL</span>
|
|
<span style="font-size: 10px; opacity: 0.5;">AIM: MOUSE</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Start Screen Overlay -->
|
|
<div id="start-screen" class="glass-panel hud-overlay">
|
|
<div class="title">STAR EXPLORER</div>
|
|
<div class="subtitle">INTERSTELLAR CIVILIZATION LOG</div>
|
|
|
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
<button class="btn" onclick="initGame()" style="width: 200px; font-size: 1.2rem; padding: 15px;">Initialize</button>
|
|
</div>
|
|
|
|
<div class="controls-hint">
|
|
<p><span class="key">W</span> <span class="key">A</span> <span class="key">S</span> <span class="key">D</span> 或 <span class="key">方向键</span> : 推进 / 移动</p>
|
|
<p><span class="key">MOUSE</span> : 瞄准</p>
|
|
<p><span class="key">LEFT CLICK</span> : 发射激光</p>
|
|
<p><span class="key">SCROLL</span> : 调整视角</p>
|
|
<p style="margin-top: 10px; color: #fff;">靠近星球进行扫描,收集资源。避开红色敌对目标,或摧毁它们。</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
/*
|
|
* ------------------------------------------------------------------
|
|
* AUDIO SYSTEM (Procedural Synthesis via Web Audio API)
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
const AudioSys = {
|
|
ctx: null,
|
|
init: function() {
|
|
if (!this.ctx) {
|
|
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
this.playAmbient();
|
|
}
|
|
},
|
|
playTone: function(freq, type, duration, vol = 0.1) {
|
|
if (!this.ctx) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
|
|
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + duration);
|
|
},
|
|
playLaser: function() {
|
|
if (!this.ctx) return;
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(800, this.ctx.currentTime);
|
|
osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.15);
|
|
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15);
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
osc.stop(this.ctx.currentTime + 0.2);
|
|
},
|
|
playExplosion: function() {
|
|
this.playTone(100, 'square', 0.4, 0.15);
|
|
this.playTone(50, 'sawtooth', 0.5, 0.2);
|
|
},
|
|
playCollect: function() {
|
|
this.playTone(1200, 'sine', 0.1, 0.1);
|
|
setTimeout(() => this.playTone(1800, 'sine', 0.15, 0.1), 80);
|
|
},
|
|
playAlert: function() {
|
|
this.playTone(400, 'square', 0.3, 0.1);
|
|
setTimeout(() => this.playTone(350, 'square', 0.3, 0.1), 300);
|
|
},
|
|
playAmbient: function() {
|
|
const osc = this.ctx.createOscillator();
|
|
const gain = this.ctx.createGain();
|
|
osc.type = 'triangle';
|
|
osc.frequency.value = 40;
|
|
gain.gain.value = 0.02;
|
|
osc.connect(gain);
|
|
gain.connect(this.ctx.destination);
|
|
osc.start();
|
|
}
|
|
};
|
|
|
|
/*
|
|
* ------------------------------------------------------------------
|
|
* GAME LOGIC & STATE
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
const Game = {
|
|
state: 'start',
|
|
score: 0,
|
|
crystals: 0,
|
|
hull: 100,
|
|
maxHull: 100,
|
|
shield: 100,
|
|
maxShield: 100,
|
|
resources: 0,
|
|
scrap: 0,
|
|
|
|
speedLevel: 1,
|
|
shieldLevel: 1,
|
|
laserLevel: 1,
|
|
|
|
enemies: [],
|
|
projectiles: [],
|
|
particles: [],
|
|
planets: [],
|
|
inCombat: false,
|
|
|
|
upgrade: function(type) {
|
|
let cost = 0;
|
|
if (type === 'engine') {
|
|
cost = 100;
|
|
if (this.resources >= cost) {
|
|
this.resources -= cost;
|
|
this.speedLevel++;
|
|
this.notify("Engines Upgraded!", "cyan");
|
|
AudioSys.playTone(600, 'triangle', 0.2);
|
|
} else { this.notify("Insufficient Resources", "red"); }
|
|
} else if (type === 'shield') {
|
|
cost = 150;
|
|
if (this.resources >= cost) {
|
|
this.resources -= cost;
|
|
this.maxShield += 20;
|
|
this.shield = this.maxShield;
|
|
this.shieldLevel++;
|
|
this.notify("Shields Reinforced!", "magenta");
|
|
AudioSys.playTone(800, 'triangle', 0.2);
|
|
} else { this.notify("Insufficient Resources", "red"); }
|
|
} else if (type === 'laser') {
|
|
cost = 200;
|
|
if (this.resources >= cost) {
|
|
this.resources -= cost;
|
|
this.laserLevel++;
|
|
this.notify("Weapons Upgraded!", "orange");
|
|
AudioSys.playTone(1000, 'triangle', 0.2);
|
|
} else { this.notify("Insufficient Resources", "red"); }
|
|
}
|
|
this.updateUI();
|
|
},
|
|
|
|
notify: function(msg, color = "white") {
|
|
const container = document.getElementById('notifications');
|
|
const el = document.createElement('div');
|
|
el.className = 'notify-msg';
|
|
el.innerText = msg;
|
|
el.style.borderColor = color;
|
|
el.style.color = color;
|
|
container.appendChild(el);
|
|
|
|
gsap.to(el, { opacity: 1, y: 0, duration: 0.5, ease: 'back.out' });
|
|
gsap.to(el, { opacity: 0, y: -20, duration: 0.5, delay: 2, onComplete: () => el.remove() });
|
|
},
|
|
|
|
start: function() {
|
|
this.state = 'playing';
|
|
this.hull = 100;
|
|
this.shield = 100;
|
|
this.resources = 0;
|
|
this.crystals = 0;
|
|
this.enemies = [];
|
|
this.projectiles = [];
|
|
this.planets = [];
|
|
this.inCombat = false;
|
|
|
|
Renderer.camera.position.set(0, 20, 30);
|
|
Renderer.camera.lookAt(0, 0, 0);
|
|
|
|
Renderer.scene.children = Renderer.scene.children.filter(c => c.name === 'StarSystem' || c.name === 'AmbientLight' || c.name === 'SunLight');
|
|
|
|
World.generate();
|
|
|
|
document.getElementById('start-screen').style.display = 'none';
|
|
this.updateUI();
|
|
|
|
AudioSys.init();
|
|
AudioSys.playTone(440, 'sine', 0.5);
|
|
},
|
|
|
|
gameOver: function() {
|
|
this.state = 'gameover';
|
|
this.notify("CRITICAL FAILURE", "red");
|
|
AudioSys.playExplosion();
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('start-screen').style.display = 'flex';
|
|
document.querySelector('#start-screen .title').innerText = "SIGNAL LOST";
|
|
document.querySelector('#start-screen .subtitle').innerText = `Final Score: ${Math.floor(this.score)}`;
|
|
document.querySelector('#start-screen button').innerText = "Reboot System";
|
|
}, 2000);
|
|
},
|
|
|
|
updateUI: function() {
|
|
document.getElementById('val-shield').innerText = Math.floor(this.shield) + '%';
|
|
document.getElementById('bar-shield').style.width = (this.shield / this.maxShield * 100) + '%';
|
|
document.getElementById('val-hull').innerText = Math.floor(this.hull) + '%';
|
|
document.getElementById('bar-hull').style.width = (this.hull / this.maxHull * 100) + '%';
|
|
document.getElementById('val-res').innerText = Math.floor(this.resources);
|
|
document.getElementById('val-crystals').innerText = this.crystals;
|
|
|
|
document.getElementById('btn-up-engine').disabled = this.resources < 100;
|
|
document.getElementById('btn-up-shield').disabled = this.resources < 150;
|
|
document.getElementById('btn-up-laser').disabled = this.resources < 200;
|
|
|
|
if (Physics.velocity) {
|
|
const speed = Physics.velocity.length();
|
|
document.getElementById('val-speed').innerText = speed.toFixed(1);
|
|
}
|
|
},
|
|
|
|
updateCombatState: function(hasHostiles) {
|
|
if (hasHostiles && !this.inCombat) {
|
|
this.inCombat = true;
|
|
const el = document.getElementById('combat-alert');
|
|
el.style.display = 'block';
|
|
gsap.to(el, { opacity: 1, duration: 0.5, yoyo: true, repeat: 3 });
|
|
AudioSys.playAlert();
|
|
} else if (!hasHostiles && this.inCombat) {
|
|
this.inCombat = false;
|
|
const el = document.getElementById('combat-alert');
|
|
gsap.to(el, { opacity: 0, duration: 0.5, onComplete: () => { el.style.display = 'none'; } });
|
|
}
|
|
},
|
|
|
|
updateStatus: function(text) {
|
|
const el = document.getElementById('val-status');
|
|
if (el) el.innerText = text;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* ------------------------------------------------------------------
|
|
* PHYSICS
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
const Physics = {
|
|
velocity: new THREE.Vector3(),
|
|
rotation: 0,
|
|
thrust: 0,
|
|
drag: 0.98,
|
|
rotateSpeed: 0.03,
|
|
|
|
update: function(dt) {
|
|
if (Game.state !== 'playing') return;
|
|
|
|
this.velocity.multiplyScalar(this.drag);
|
|
|
|
if (this.thrust > 0) {
|
|
const accel = 0.5 + (Game.speedLevel * 0.1);
|
|
const vector = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation);
|
|
this.velocity.add(vector.multiplyScalar(this.thrust * accel * dt));
|
|
|
|
Renderer.spawnThrusterParticles();
|
|
if (Math.random() > 0.8) AudioSys.playTone(100 + Math.random()*50, 'sawtooth', 0.05, 0.02);
|
|
}
|
|
|
|
if (this.movingLeft) this.rotation += this.rotateSpeed;
|
|
if (this.movingRight) this.rotation -= this.rotateSpeed;
|
|
|
|
if (Renderer.playerMesh) {
|
|
Renderer.playerMesh.rotation.y = this.rotation;
|
|
Renderer.playerMesh.position.add(this.velocity.clone().multiplyScalar(1));
|
|
}
|
|
|
|
if (Renderer.camera && Renderer.playerMesh) {
|
|
Renderer.camera.position.x += (Renderer.playerMesh.position.x - Renderer.camera.position.x) * 0.05;
|
|
Renderer.camera.position.z += (Renderer.playerMesh.position.z - Renderer.camera.position.z) * 0.05;
|
|
Renderer.camera.position.y += (20 + Math.abs(this.velocity.length()) - Renderer.camera.position.y) * 0.05;
|
|
Renderer.camera.lookAt(Renderer.playerMesh.position);
|
|
}
|
|
|
|
if (Game.enemies) Game.enemies.forEach(e => e.update(dt));
|
|
if (Game.projectiles) Game.projectiles.forEach(p => p.update(dt));
|
|
if (Game.particles) Game.particles.forEach(p => p.update(dt));
|
|
|
|
Game.enemies = Game.enemies.filter(e => e.active);
|
|
Game.projectiles = Game.projectiles.filter(p => p.active);
|
|
Game.particles = Game.particles.filter(p => p.active);
|
|
|
|
if (World) World.spawnLogic();
|
|
|
|
Game.updateUI();
|
|
}
|
|
};
|
|
|
|
/*
|
|
* ------------------------------------------------------------------
|
|
* RENDERER (Three.js Setup)
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
const Renderer = {
|
|
scene: null,
|
|
camera: null,
|
|
renderer: null,
|
|
playerMesh: null,
|
|
raycaster: new THREE.Raycaster(),
|
|
mouse: new THREE.Vector2(),
|
|
|
|
init: function() {
|
|
const canvas = document.getElementById('game-canvas');
|
|
this.scene = new THREE.Scene();
|
|
this.scene.fog = new THREE.FogExp2(0x05070a, 0.002);
|
|
|
|
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
this.camera.position.set(0, 20, 30);
|
|
|
|
this.renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true, alpha: false });
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 1.5);
|
|
ambientLight.name = 'AmbientLight';
|
|
this.scene.add(ambientLight);
|
|
|
|
const sunLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
sunLight.position.set(100, 100, 50);
|
|
sunLight.name = 'SunLight';
|
|
this.scene.add(sunLight);
|
|
|
|
this.createStarField();
|
|
this.createPlayerShip();
|
|
|
|
window.addEventListener('resize', () => {
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (Game.state !== 'playing') return;
|
|
switch(e.key.toLowerCase()) {
|
|
case 'w': case 'arrowup': Physics.thrust = 1; Game.updateStatus("Accelerating"); break;
|
|
case 's': case 'arrowdown': Physics.thrust = -0.5; Game.updateStatus("Retro Thrusters"); break;
|
|
case 'a': case 'arrowleft': Physics.movingLeft = true; break;
|
|
case 'd': case 'arrowright': Physics.movingRight = true; break;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keyup', (e) => {
|
|
if (Game.state !== 'playing') return;
|
|
switch(e.key.toLowerCase()) {
|
|
case 'w': case 'arrowup': case 's': case 'arrowdown':
|
|
Physics.thrust = 0; Game.updateStatus("Drifting"); break;
|
|
case 'a': case 'arrowleft': Physics.movingLeft = false; break;
|
|
case 'd': case 'arrowright': Physics.movingRight = false; break;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('mousemove', (e) => {
|
|
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
|
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
|
});
|
|
|
|
window.addEventListener('mousedown', (e) => {
|
|
if (Game.state !== 'playing') return;
|
|
if (e.button === 0) this.fireWeapon();
|
|
});
|
|
|
|
window.addEventListener('wheel', (e) => {
|
|
if (Game.state !== 'playing') return;
|
|
const delta = e.deltaY * 0.05;
|
|
this.camera.position.z += delta;
|
|
this.camera.position.y += delta * 0.5;
|
|
});
|
|
},
|
|
|
|
createPlayerShip: function() {
|
|
const geometry = new THREE.ConeGeometry(1, 3, 8);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: 0x222222,
|
|
emissive: 0x00f3ff,
|
|
emissiveIntensity: 0.2,
|
|
flatShading: true
|
|
});
|
|
this.playerMesh = new THREE.Mesh(geometry, material);
|
|
this.playerMesh.rotation.x = Math.PI / 2;
|
|
this.scene.add(this.playerMesh);
|
|
|
|
this.aimPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
|
|
},
|
|
|
|
fireWeapon: function() {
|
|
if (!this.playerMesh) return;
|
|
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
const target = new THREE.Vector3();
|
|
this.raycaster.ray.intersectPlane(this.aimPlane, target);
|
|
|
|
if (!target) return;
|
|
|
|
const start = this.playerMesh.position.clone();
|
|
const dir = new THREE.Vector3().subVectors(target, start).normalize();
|
|
dir.x += (Math.random() - 0.5) * (0.1 - (Game.laserLevel * 0.01));
|
|
dir.z += (Math.random() - 0.5) * (0.1 - (Game.laserLevel * 0.01));
|
|
|
|
const p = {
|
|
mesh: null,
|
|
active: true,
|
|
velocity: dir.multiplyScalar(2 + (Game.laserLevel * 0.2)),
|
|
life: 100,
|
|
isPlayer: true,
|
|
|
|
update: function(dt) {
|
|
if (!this.active) return;
|
|
this.mesh.position.add(this.velocity);
|
|
this.life--;
|
|
if (this.life <= 0) {
|
|
this.active = false;
|
|
this.cleanup();
|
|
return;
|
|
}
|
|
|
|
for (let e of Game.enemies) {
|
|
if (!e.active) continue;
|
|
if (this.mesh.position.distanceTo(e.mesh.position) < 3) {
|
|
this.active = false;
|
|
this.cleanup();
|
|
e.takeDamage(20 + (Game.laserLevel * 5));
|
|
Game.resources += 5;
|
|
Game.score += 10;
|
|
Renderer.spawnExplosion(this.mesh.position, 0x00f3ff, 10);
|
|
AudioSys.playTone(600 + Math.random()*200, 'square', 0.1, 0.05);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (let pl of Game.planets) {
|
|
if (this.mesh.position.distanceTo(pl.mesh.position) < pl.radius + 1) {
|
|
this.active = false;
|
|
this.cleanup();
|
|
Renderer.spawnExplosion(this.mesh.position, 0xffaa00, 5);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
cleanup: function() {
|
|
if (this.mesh) {
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
}
|
|
}
|
|
};
|
|
|
|
const geo = new THREE.BoxGeometry(0.2, 0.2, 2);
|
|
const mat = new THREE.MeshBasicMaterial({ color: 0x00f3ff });
|
|
p.mesh = new THREE.Mesh(geo, mat);
|
|
p.mesh.position.copy(start);
|
|
p.mesh.lookAt(target);
|
|
Renderer.scene.add(p.mesh);
|
|
|
|
Game.projectiles.push(p);
|
|
AudioSys.playLaser();
|
|
|
|
Physics.velocity.add(new THREE.Vector3(0,0,1).applyAxisAngle(new THREE.Vector3(0,1,0), Physics.rotation)).multiplyScalar(-0.1);
|
|
},
|
|
|
|
spawnThrusterParticles: function() {
|
|
if (Math.random() > 0.5) return;
|
|
|
|
const p = {
|
|
mesh: null,
|
|
active: true,
|
|
life: 20 + Math.random() * 10,
|
|
velocity: new THREE.Vector3((Math.random()-0.5)*0.1, 0, 1).applyAxisAngle(new THREE.Vector3(0,1,0), Physics.rotation),
|
|
|
|
update: function(dt) {
|
|
this.mesh.position.add(this.velocity);
|
|
this.life--;
|
|
this.mesh.material.opacity = this.life / 30;
|
|
if (this.life <= 0) {
|
|
this.active = false;
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
}
|
|
}
|
|
};
|
|
|
|
const geo = new THREE.PlaneGeometry(0.5, 0.5);
|
|
const mat = new THREE.MeshBasicMaterial({ color: 0xff5500, transparent: true, opacity: 0.8, side: THREE.DoubleSide });
|
|
p.mesh = new THREE.Mesh(geo, mat);
|
|
p.mesh.position.copy(Renderer.playerMesh.position);
|
|
p.mesh.position.x -= Math.sin(Physics.rotation) * 0.5;
|
|
p.mesh.position.z -= Math.cos(Physics.rotation) * 0.5;
|
|
p.mesh.position.add(new THREE.Vector3((Math.random()-0.5)*0.5, 0, (Math.random()-0.5)*0.5));
|
|
p.mesh.lookAt(Renderer.camera.position);
|
|
Renderer.scene.add(p.mesh);
|
|
Game.particles.push(p);
|
|
},
|
|
|
|
spawnExplosion: function(pos, color, count) {
|
|
if (count > 5) AudioSys.playExplosion();
|
|
for(let i=0; i<count; i++) {
|
|
const p = {
|
|
mesh: null,
|
|
active: true,
|
|
life: 30 + Math.random()*20,
|
|
velocity: new THREE.Vector3((Math.random()-0.5), (Math.random()-0.5), (Math.random()-0.5)).normalize().multiplyScalar(0.3),
|
|
update: function(dt) {
|
|
this.mesh.position.add(this.velocity);
|
|
this.life--;
|
|
if (this.life < 10) this.mesh.scale.multiplyScalar(0.8);
|
|
this.mesh.material.opacity = this.life / 50;
|
|
if (this.life <= 0) {
|
|
this.active = false;
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
}
|
|
}
|
|
};
|
|
const geo = new THREE.TetrahedronGeometry(0.3);
|
|
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1 });
|
|
p.mesh = new THREE.Mesh(geo, mat);
|
|
p.mesh.position.copy(pos);
|
|
Renderer.scene.add(p.mesh);
|
|
Game.particles.push(p);
|
|
}
|
|
},
|
|
|
|
createStarField: function() {
|
|
const geometry = new THREE.BufferGeometry();
|
|
const count = 1500;
|
|
const positions = new Float32Array(count * 3);
|
|
for(let i=0; i<count*3; i++) {
|
|
positions[i] = (Math.random() - 0.5) * 1000;
|
|
}
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
const material = new THREE.PointsMaterial({ size: 1.5, color: 0xffffff, transparent: true, opacity: 0.8 });
|
|
const stars = new THREE.Points(geometry, material);
|
|
stars.name = 'StarSystem';
|
|
this.scene.add(stars);
|
|
},
|
|
|
|
render: function() {
|
|
requestAnimationFrame(this.render.bind(this));
|
|
|
|
if (Game.state === 'playing') {
|
|
if (Physics.thrust !== 0 || Physics.movingLeft || Physics.movingRight) {
|
|
Physics.update(1);
|
|
}
|
|
}
|
|
|
|
const stars = this.scene.getObjectByName('StarSystem');
|
|
if (stars) {
|
|
stars.rotation.y += 0.0002;
|
|
stars.rotation.x = Math.sin(Date.now() * 0.0001) * 0.05;
|
|
}
|
|
|
|
if (Renderer.playerMesh && Renderer.camera && Game.state === 'playing') {
|
|
let shake = 0;
|
|
if (Game.hull < 30) shake = (Math.random()-0.5) * 0.5;
|
|
Renderer.camera.position.x += (Renderer.playerMesh.position.x + shake - Renderer.camera.position.x) * 0.1;
|
|
Renderer.camera.position.z += (Renderer.playerMesh.position.z + shake - Renderer.camera.position.z) * 0.1;
|
|
Renderer.camera.position.y += ((20 + Math.abs(Physics.velocity.length()*2)) - Renderer.camera.position.y) * 0.05;
|
|
}
|
|
|
|
this.renderer.render(this.scene, this.camera);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* ------------------------------------------------------------------
|
|
* WORLD GENERATION
|
|
* ------------------------------------------------------------------
|
|
*/
|
|
const World = {
|
|
spawnTimer: 0,
|
|
|
|
generate: function() {
|
|
this.spawnCluster(new THREE.Vector3(0, 0, -50));
|
|
this.spawnCluster(new THREE.Vector3(100, 0, -150));
|
|
this.spawnCluster(new THREE.Vector3(-80, 0, -200));
|
|
},
|
|
|
|
spawnCluster: function(center) {
|
|
for (let i = 0; i < 3; i++) {
|
|
const radius = 5 + Math.random() * 10;
|
|
const pos = center.clone().add(new THREE.Vector3((Math.random()-0.5)*60, (Math.random()-0.5)*10, (Math.random()-0.5)*60));
|
|
this.createPlanet(pos, radius);
|
|
}
|
|
},
|
|
|
|
createPlanet: function(pos, radius) {
|
|
const geometry = new THREE.IcosahedronGeometry(radius, 1);
|
|
const color = Math.random() > 0.7 ? 0x00f3ff : (Math.random() > 0.5 ? 0xbd00ff : 0xffaa00);
|
|
const material = new THREE.MeshPhongMaterial({
|
|
color: color,
|
|
emissive: color,
|
|
emissiveIntensity: 0.1,
|
|
flatShading: true,
|
|
shininess: 30
|
|
});
|
|
const mesh = new THREE.Mesh(geometry, material);
|
|
mesh.position.copy(pos);
|
|
|
|
if (Math.random() > 0.6) {
|
|
const ringGeo = new THREE.RingGeometry(radius * 1.4, radius * 1.8, 32);
|
|
const ringMat = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide, transparent: true, opacity: 0.3 });
|
|
const ring = new THREE.Mesh(ringGeo, ringMat);
|
|
ring.rotation.x = Math.PI / 2;
|
|
mesh.add(ring);
|
|
}
|
|
|
|
Renderer.scene.add(mesh);
|
|
|
|
const planetObj = {
|
|
mesh: mesh,
|
|
radius: radius,
|
|
type: Math.random() > 0.3 ? 'resource' : 'empty',
|
|
active: true,
|
|
collected: false,
|
|
angle: Math.random() * Math.PI * 2,
|
|
speed: (Math.random() - 0.5) * 0.005,
|
|
radiusOrbit: Math.random() * 10
|
|
};
|
|
|
|
Game.planets.push(planetObj);
|
|
},
|
|
|
|
spawnLogic: function() {
|
|
this.spawnTimer++;
|
|
if (this.spawnTimer > 300) {
|
|
this.spawnTimer = 0;
|
|
const dist = Renderer.playerMesh.position.length();
|
|
if (dist > 50) {
|
|
this.spawnEnemy();
|
|
}
|
|
}
|
|
},
|
|
|
|
spawnEnemy: function() {
|
|
if (Game.enemies.length > 6) return;
|
|
|
|
const geo = new THREE.OctahedronGeometry(1.5, 0);
|
|
const mat = new THREE.MeshPhongMaterial({ color: 0xff3333, emissive: 0xff0000, emissiveIntensity: 0.5 });
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const dist = 20 + Math.random() * 20;
|
|
const pos = Renderer.playerMesh.position.clone().add(new THREE.Vector3(Math.cos(angle)*dist, (Math.random()-0.5)*5, Math.sin(angle)*dist));
|
|
mesh.position.copy(pos);
|
|
Renderer.scene.add(mesh);
|
|
|
|
const enemy = {
|
|
mesh: mesh,
|
|
active: true,
|
|
hp: 50,
|
|
type: 'drone',
|
|
fireTimer: 0,
|
|
|
|
update: function(dt) {
|
|
if (!this.active) return;
|
|
|
|
const dir = new THREE.Vector3().subVectors(Renderer.playerMesh.position, this.mesh.position).normalize();
|
|
this.mesh.position.add(dir.multiplyScalar(0.1));
|
|
this.mesh.lookAt(Renderer.playerMesh.position);
|
|
this.mesh.rotation.x += 0.05;
|
|
|
|
this.fireTimer++;
|
|
if (this.fireTimer > 180) {
|
|
this.fireTimer = 0;
|
|
this.shoot();
|
|
}
|
|
|
|
const d = this.mesh.position.distanceTo(Renderer.playerMesh.position);
|
|
if (d < 2) {
|
|
Game.shield -= 5;
|
|
Game.notify("IMPACT WARNING", "red");
|
|
this.takeDamage(100);
|
|
Renderer.spawnExplosion(this.mesh.position, 0xff3333, 20);
|
|
}
|
|
},
|
|
|
|
shoot: function() {
|
|
const p = {
|
|
mesh: null,
|
|
active: true,
|
|
life: 100,
|
|
velocity: new THREE.Vector3().subVectors(Renderer.playerMesh.position, this.mesh.position).normalize().multiplyScalar(0.4),
|
|
update: function(dt) {
|
|
this.mesh.position.add(this.velocity);
|
|
this.life--;
|
|
if (this.life <= 0) {
|
|
this.active = false;
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
}
|
|
if (this.mesh.position.distanceTo(Renderer.playerMesh.position) < 1.5) {
|
|
Game.shield -= 10;
|
|
Game.updateStatus("UNDER FIRE");
|
|
this.active = false;
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
Renderer.spawnExplosion(this.mesh.position, 0xffaa00, 5);
|
|
AudioSys.playTone(150, 'square', 0.2);
|
|
}
|
|
}
|
|
};
|
|
const geo = new THREE.BoxGeometry(0.3, 0.3, 1);
|
|
const mat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
|
|
p.mesh = new THREE.Mesh(geo, mat);
|
|
p.mesh.position.copy(this.mesh.position);
|
|
p.mesh.lookAt(Renderer.playerMesh.position);
|
|
Renderer.scene.add(p.mesh);
|
|
Game.projectiles.push(p);
|
|
AudioSys.playTone(200, 'sawtooth', 0.1, 0.05);
|
|
},
|
|
|
|
takeDamage: function(amount) {
|
|
this.hp -= amount;
|
|
if (this.hp <= 0) {
|
|
this.active = false;
|
|
this.mesh.visible = false;
|
|
Renderer.scene.remove(this.mesh);
|
|
Game.score += 50;
|
|
Game.resources += 20;
|
|
Game.crystals += (Math.random() > 0.8 ? 1 : 0);
|
|
Game.updateUI();
|
|
if (Math.random() > 0.8) {
|
|
Game.shield = Math.min(Game.maxShield, Game.shield + 10);
|
|
Game.notify("Shield Bonus", "cyan");
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Game.enemies.push(enemy);
|
|
}
|
|
};
|
|
|
|
// Planet Collision & Collection Logic
|
|
setInterval(() => {
|
|
if (Game.state !== 'playing' || !Renderer.playerMesh) return;
|
|
let nearPlanet = false;
|
|
const playerPos = Renderer.playerMesh.position;
|
|
|
|
Game.planets.forEach(p => {
|
|
if (!p.active) return;
|
|
p.mesh.rotation.y += 0.005;
|
|
|
|
const d = playerPos.distanceTo(p.mesh.position);
|
|
if (d < p.radius + 5) {
|
|
nearPlanet = true;
|
|
if (!p.collected) {
|
|
p.collected = true;
|
|
p.mesh.material.emissiveIntensity = 0;
|
|
const gain = Math.floor(p.radius * 2);
|
|
Game.resources += gain;
|
|
Game.notify(`Scanned: +${gain}`, "cyan");
|
|
AudioSys.playCollect();
|
|
Renderer.spawnExplosion(p.mesh.position, 0x00f3ff, 15);
|
|
}
|
|
}
|
|
});
|
|
|
|
if (nearPlanet && Game.state === 'playing') Game.updateStatus("Scanning...");
|
|
else if (Game.state === 'playing' && Physics.thrust === 0) Game.updateStatus("Drifting");
|
|
|
|
// Check Health
|
|
if (Game.shield <= 0) {
|
|
Game.shield = 0;
|
|
Game.hull -= 0.2;
|
|
}
|
|
if (Game.hull <= 0) {
|
|
Game.hull = 0;
|
|
Game.gameOver();
|
|
}
|
|
|
|
// Combat Alert
|
|
const hasHostiles = Game.enemies.length > 0;
|
|
Game.updateCombatState(hasHostiles);
|
|
}, 100);
|
|
|
|
// Init
|
|
function initGame() {
|
|
if (!Renderer.renderer) {
|
|
Renderer.init();
|
|
Game.start();
|
|
Renderer.render();
|
|
} else {
|
|
Game.start();
|
|
}
|
|
}
|
|
|
|
console.log("%c SYSTEM READY ", "background: #00f3ff; color: #000; font-size: 20px; font-weight: bold;");
|
|
</script>
|
|
</body>
|
|
</html>
|