1248 lines
54 KiB
HTML
1248 lines
54 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>星际文明探索者 | Interstellar Explorer</title>
|
||
<style>
|
||
/* --- 基础样式 --- */
|
||
:root {
|
||
--primary-color: #00f3ff;
|
||
--secondary-color: #ff0055;
|
||
--bg-glass: rgba(12, 12, 28, 0.75);
|
||
--border-glass: 1px solid rgba(255, 255, 255, 0.15);
|
||
--text-glow: 0 0 10px rgba(0, 243, 255, 0.5);
|
||
--font-main: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
background-color: #050510;
|
||
font-family: var(--font-main);
|
||
color: white;
|
||
user-select: none;
|
||
}
|
||
|
||
canvas {
|
||
display: block;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
outline: none;
|
||
}
|
||
|
||
/* --- UI 通用组件 --- */
|
||
.ui-layer {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
z-index: 10;
|
||
}
|
||
|
||
.hud-panel {
|
||
background: var(--bg-glass);
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
border: var(--border-glass);
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
pointer-events: auto;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
.btn {
|
||
background: rgba(0, 243, 255, 0.1);
|
||
border: 1px solid var(--primary-color);
|
||
color: var(--primary-color);
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
font-family: var(--font-main);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
transition: all 0.2s;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.btn:hover {
|
||
background: var(--primary-color);
|
||
color: #000;
|
||
box-shadow: 0 0 15px var(--primary-color);
|
||
}
|
||
|
||
.btn-danger {
|
||
border-color: var(--secondary-color);
|
||
color: var(--secondary-color);
|
||
background: rgba(255, 0, 85, 0.1);
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: var(--secondary-color);
|
||
color: white;
|
||
box-shadow: 0 0 15px var(--secondary-color);
|
||
}
|
||
|
||
/* --- HUD 布局 --- */
|
||
#hud-top-left {
|
||
position: absolute;
|
||
top: 20px;
|
||
left: 20px;
|
||
width: 240px;
|
||
}
|
||
|
||
#hud-top-right {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
text-align: right;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#radar-container {
|
||
position: relative;
|
||
pointer-events: auto;
|
||
display: inline-block;
|
||
}
|
||
|
||
#hud-center-msg {
|
||
position: absolute;
|
||
top: 20%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
text-align: center;
|
||
font-size: 24px;
|
||
text-shadow: var(--text-glow);
|
||
opacity: 0;
|
||
transition: opacity 0.5s;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#hud-bottom-center {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
/* --- 状态条 --- */
|
||
.stat-row { margin-bottom: 8px; }
|
||
.stat-label {
|
||
font-size: 11px;
|
||
color: #aaa;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
margin-top: 4px;
|
||
}
|
||
.progress-fill { height: 100%; width: 100%; transition: width 0.3s ease-out; }
|
||
.hp-fill { background: var(--secondary-color); box-shadow: 0 0 8px var(--secondary-color); }
|
||
.shield-fill { background: var(--primary-color); box-shadow: 0 0 8px var(--primary-color); }
|
||
.xp-fill { background: #ffcc00; }
|
||
|
||
/* --- 模态框 --- */
|
||
.modal-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 100;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
.modal-overlay.active {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.modal-content {
|
||
width: 600px;
|
||
max-width: 90%;
|
||
background: rgba(16, 20, 35, 0.95);
|
||
border: 1px solid var(--primary-color);
|
||
padding: 30px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 0 40px rgba(0, 243, 255, 0.15);
|
||
position: relative;
|
||
transform: scale(0.9);
|
||
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||
}
|
||
.modal-overlay.active .modal-content { transform: scale(1); }
|
||
|
||
h2 { margin-top: 0; color: var(--primary-color); text-transform: uppercase; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 10px; letter-spacing: 2px; font-size: 24px;}
|
||
|
||
.upgrade-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 15px;
|
||
margin: 25px 0;
|
||
}
|
||
.upgrade-card {
|
||
background: rgba(255,255,255,0.03);
|
||
padding: 15px 10px;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
transition: all 0.2s;
|
||
}
|
||
.upgrade-card:hover {
|
||
border-color: var(--primary-color);
|
||
background: rgba(0, 243, 255, 0.05);
|
||
transform: translateY(-2px);
|
||
}
|
||
.upgrade-icon { font-size: 32px; display: block; margin-bottom: 10px; }
|
||
.upgrade-name { font-size: 14px; font-weight: bold; color: #eee; }
|
||
.upgrade-level { font-size: 12px; color: #666; margin-top: 5px; }
|
||
.upgrade-cost { font-size: 12px; color: #ffcc00; margin-top: 5px; display: block;}
|
||
|
||
/* --- 扫描面板细节 --- */
|
||
.scan-details {
|
||
display: grid;
|
||
grid-template-columns: auto 1fr;
|
||
gap: 8px 20px;
|
||
margin: 20px 0;
|
||
font-size: 14px;
|
||
background: rgba(0,0,0,0.3);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
}
|
||
.scan-label { color: #aaa; }
|
||
.scan-value { color: #fff; text-align: right; font-family: 'Courier New', monospace; }
|
||
|
||
/* --- 交互提示 --- */
|
||
.interaction-key {
|
||
display: inline-block;
|
||
border: 1px solid rgba(255,255,255,0.4);
|
||
border-radius: 4px;
|
||
padding: 2px 6px;
|
||
font-size: 12px;
|
||
background: rgba(255,255,255,0.1);
|
||
margin-right: 5px;
|
||
font-family: monospace;
|
||
min-width: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* --- 准星 --- */
|
||
#crosshair {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 40px;
|
||
height: 40px;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
z-index: 5;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
#crosshair::before, #crosshair::after {
|
||
content: '';
|
||
position: absolute;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
transition: all 0.2s;
|
||
}
|
||
#crosshair::before { width: 2px; height: 100%; }
|
||
#crosshair::after { height: 2px; width: 100%; }
|
||
#crosshair.hovering::before, #crosshair.hovering::after { background: var(--primary-color); height: 120%; width: 120%; }
|
||
#crosshair-hover-text {
|
||
position: absolute;
|
||
top: -25px;
|
||
color: var(--primary-color);
|
||
font-size: 12px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
text-shadow: 0 0 5px black;
|
||
}
|
||
#crosshair.hovering #crosshair-hover-text { opacity: 1; }
|
||
|
||
/* --- 加载遮罩 --- */
|
||
#loader {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #000;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
align-items: center;
|
||
transition: opacity 1s ease-out;
|
||
}
|
||
.loader-title { font-size: 32px; letter-spacing: 8px; font-weight: 300; margin-bottom: 20px; text-shadow: 0 0 20px var(--primary-color); }
|
||
.loader-bar { width: 200px; height: 2px; background: #333; position: relative; overflow: hidden; }
|
||
.loader-progress {
|
||
position: absolute; left: 0; top: 0; height: 100%; width: 0%;
|
||
background: var(--primary-color);
|
||
box-shadow: 0 0 10px var(--primary-color);
|
||
animation: load 1.5s ease-in-out forwards;
|
||
}
|
||
@keyframes load { 100% { width: 100%; } }
|
||
</style>
|
||
|
||
<!-- 引入外部库 (CDN) -->
|
||
<!-- Three.js -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||
|
||
<!-- 修复:使用经典的 cannon.js (0.6.2) 确保兼容全局变量 CANNON -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
|
||
|
||
<!-- GSAP (动画) -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
||
|
||
</head>
|
||
<body>
|
||
|
||
<!-- 加载界面 -->
|
||
<div id="loader">
|
||
<div class="loader-title">INTERSTELLAR</div>
|
||
<div style="font-size: 12px; color: #666; margin-bottom: 20px;">初始化物理引擎...</div>
|
||
<div class="loader-bar"><div class="loader-progress"></div></div>
|
||
</div>
|
||
|
||
<!-- 3D 画布 -->
|
||
<div id="canvas-container"></div>
|
||
|
||
<!-- 准星 -->
|
||
<div id="crosshair"><div id="crosshair-hover-text">按 F 扫描</div></div>
|
||
|
||
<!-- UI 层 -->
|
||
<div class="ui-layer" id="ui-layer">
|
||
|
||
<!-- 左上角:状态 -->
|
||
<div id="hud-top-left" class="hud-panel">
|
||
<div class="stat-row">
|
||
<div class="stat-label"><span>结构完整性 (HP)</span> <span id="hp-val">100%</span></div>
|
||
<div class="progress-bar"><div id="hp-bar" class="progress-fill hp-fill"></div></div>
|
||
</div>
|
||
<div class="stat-row">
|
||
<div class="stat-label"><span>能量护盾</span> <span id="shield-val">100%</span></div>
|
||
<div class="progress-bar"><div id="shield-bar" class="progress-fill shield-fill"></div></div>
|
||
</div>
|
||
<div class="stat-row" style="margin-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top:10px;">
|
||
<div class="stat-label"><span>资源储备</span></div>
|
||
<div style="font-size: 24px; color: #ffcc00; text-shadow: 0 0 10px rgba(255, 204, 0, 0.4);">
|
||
<span id="res-count">0</span> <span style="font-size:12px; color:#888;">UNIT</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右上角:雷达 -->
|
||
<div id="hud-top-right">
|
||
<div id="radar-container" class="hud-panel" style="padding: 5px; display:inline-block;">
|
||
<div id="radar-display" style="width: 120px; height: 120px; border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; position: relative; background: rgba(0,0,0,0.5); overflow: hidden;">
|
||
<!-- 中心玩家 -->
|
||
<div style="position: absolute; top: 50%; left: 50%; width: 6px; height: 6px; background: var(--primary-color); border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 5px var(--primary-color);"></div>
|
||
<!-- 动态雷达点将通过 JS 插入 -->
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 10px; font-size: 12px; color: #888; font-family: monospace;">POS: <span id="coords">0, 0</span></div>
|
||
</div>
|
||
|
||
<!-- 中央消息 -->
|
||
<div id="hud-center-msg">
|
||
<div id="msg-title" style="font-size: 32px; font-weight: bold; margin-bottom: 10px;">警告</div>
|
||
<div id="msg-desc">侦测到敌对信号</div>
|
||
</div>
|
||
|
||
<!-- 底部中央:控制 -->
|
||
<div id="hud-bottom-center" class="hud-panel" style="padding: 10px 25px;">
|
||
<button class="btn" onclick="Game.openUpgradeMenu()">升级系统 [U]</button>
|
||
<div style="font-size: 11px; color: #666; margin-left: 10px;">
|
||
W/A/S/D 移动 • 鼠标 瞄准 • 左键 射击 • F 交互
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 扫描结果模态框 -->
|
||
<div id="scan-modal" class="modal-overlay">
|
||
<div class="modal-content">
|
||
<h2>行星分析报告</h2>
|
||
<div class="scan-details">
|
||
<span class="scan-label">行星代号:</span>
|
||
<span class="scan-value" id="scan-name">Kepler-186f</span>
|
||
|
||
<span class="scan-label">大气成分:</span>
|
||
<span class="scan-value" id="scan-atmo">氮气 78%, 氧气 21%</span>
|
||
|
||
<span class="scan-label">表面温度:</span>
|
||
<span class="scan-value" id="scan-temp">-12°C</span>
|
||
|
||
<span class="scan-label">宜居指数:</span>
|
||
<span class="scan-value" style="color: #0f0;" id="scan-habit">Class A</span>
|
||
|
||
<span class="scan-label">资源潜力:</span>
|
||
<span class="scan-value" style="color: #ffcc00;" id="scan-res">高 (晶体矿)</span>
|
||
</div>
|
||
<div style="text-align: right; margin-top: 25px;">
|
||
<button class="btn btn-danger" onclick="Game.closeScanModal()">忽略</button>
|
||
<button class="btn" onclick="Game.collectPlanetResources()">采集样本 [花费时间]</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 升级菜单模态框 -->
|
||
<div id="upgrade-modal" class="modal-overlay">
|
||
<div class="modal-content">
|
||
<h2>飞船改装中心</h2>
|
||
<div style="text-align: center; margin-bottom: 20px;">
|
||
<span style="font-size: 14px; color: #888;">当前资源储备: </span>
|
||
<span id="upgrade-res" style="color: #ffcc00; font-size: 18px; font-weight: bold;">0</span>
|
||
</div>
|
||
|
||
<div class="upgrade-grid">
|
||
<div class="upgrade-card" onclick="Game.upgradeShip('engine')">
|
||
<span class="upgrade-icon">🚀</span>
|
||
<div class="upgrade-name">引擎核心</div>
|
||
<div class="upgrade-level">Lv <span id="lvl-engine">1</span></div>
|
||
<span class="upgrade-cost">100 UNIT</span>
|
||
</div>
|
||
<div class="upgrade-card" onclick="Game.upgradeShip('shield')">
|
||
<span class="upgrade-icon">🛡️</span>
|
||
<div class="upgrade-name">偏导护盾</div>
|
||
<div class="upgrade-level">Lv <span id="lvl-shield">1</span></div>
|
||
<span class="upgrade-cost">80 UNIT</span>
|
||
</div>
|
||
<div class="upgrade-card" onclick="Game.upgradeShip('weapon')">
|
||
<span class="upgrade-icon">⚡</span>
|
||
<div class="upgrade-name">相位激光</div>
|
||
<div class="upgrade-level">Lv <span id="lvl-weapon">1</span></div>
|
||
<span class="upgrade-cost">120 UNIT</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="text-align: center;">
|
||
<button class="btn btn-danger" onclick="Game.closeUpgradeMenu()">返回航行</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 游戏结束/开始界面 -->
|
||
<div id="start-screen" class="modal-overlay active" style="background: rgba(0,0,0,0.85);">
|
||
<div class="modal-content" style="text-align: center; max-width: 500px; border: 1px solid #fff;">
|
||
<h1 style="font-size: 40px; margin-bottom: 5px; color: #fff;">星际文明探索者</h1>
|
||
<div style="width: 50px; height: 2px; background: var(--primary-color); margin: 15px auto;"></div>
|
||
<p style="color: #aaa; line-height: 1.6; font-size: 14px;">
|
||
驾驶飞船探索未知宇宙,采集稀有资源,升级战舰。<br>
|
||
驾驶物理引擎飞船,体验太空漂移。
|
||
</p>
|
||
<div style="text-align: left; background: rgba(255,255,255,0.05); padding: 15px; border-radius: 6px; margin: 25px 0; font-size: 13px; display: inline-block;">
|
||
<div style="margin-bottom: 5px;"><span class="interaction-key">W</span><span class="interaction-key">A</span><span class="interaction-key">S</span><span class="interaction-key">D</span> 移动引擎 (具有惯性)</div>
|
||
<div style="margin-bottom: 5px;"><span class="interaction-key">鼠标</span> 旋转与瞄准</div>
|
||
<div style="margin-bottom: 5px;"><span class="interaction-key">左键</span> 射击</div>
|
||
<div style="margin-bottom: 5px;"><span class="interaction-key">F</span> 扫描星球 / 采集</div>
|
||
<div><span class="interaction-key">U</span> 升级系统</div>
|
||
</div>
|
||
<br>
|
||
<button class="btn" style="font-size: 18px; padding: 12px 50px; width: 100%;" onclick="Game.start()">启动引擎</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// --- 1. 音频管理器 ---
|
||
class SoundManager {
|
||
constructor() {
|
||
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||
this.masterGain = this.ctx.createGain();
|
||
this.masterGain.gain.value = 0.3;
|
||
this.masterGain.connect(this.ctx.destination);
|
||
}
|
||
|
||
playShoot() {
|
||
if(this.ctx.state === 'suspended') this.ctx.resume();
|
||
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.3, this.ctx.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.15);
|
||
osc.connect(gain);
|
||
gain.connect(this.masterGain);
|
||
osc.start();
|
||
osc.stop(this.ctx.currentTime + 0.15);
|
||
}
|
||
|
||
playExplosion() {
|
||
if(this.ctx.state === 'suspended') this.ctx.resume();
|
||
const bufferSize = this.ctx.sampleRate * 0.4;
|
||
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
|
||
const data = buffer.getChannelData(0);
|
||
for (let i = 0; i < bufferSize; i++) {
|
||
data[i] = Math.random() * 2 - 1;
|
||
}
|
||
const noise = this.ctx.createBufferSource();
|
||
noise.buffer = buffer;
|
||
const filter = this.ctx.createBiquadFilter();
|
||
filter.type = 'lowpass';
|
||
filter.frequency.value = 800;
|
||
filter.frequency.linearRampToValueAtTime(100, this.ctx.currentTime + 0.4);
|
||
const gain = this.ctx.createGain();
|
||
gain.gain.setValueAtTime(0.5, this.ctx.currentTime);
|
||
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + 0.4);
|
||
noise.connect(filter);
|
||
filter.connect(gain);
|
||
gain.connect(this.masterGain);
|
||
noise.start();
|
||
}
|
||
|
||
playThrust() {
|
||
if(this.ctx.state === 'suspended') this.ctx.resume();
|
||
const osc = this.ctx.createOscillator();
|
||
const gain = this.ctx.createGain();
|
||
osc.type = 'triangle'; // 更柔和的引擎声
|
||
osc.frequency.setValueAtTime(60, this.ctx.currentTime);
|
||
osc.frequency.linearRampToValueAtTime(40, this.ctx.currentTime + 0.1);
|
||
gain.gain.setValueAtTime(0.05, this.ctx.currentTime);
|
||
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.1);
|
||
osc.connect(gain);
|
||
gain.connect(this.masterGain);
|
||
osc.start();
|
||
osc.stop(this.ctx.currentTime + 0.1);
|
||
}
|
||
|
||
playCollect() {
|
||
if(this.ctx.state === 'suspended') this.ctx.resume();
|
||
const osc = this.ctx.createOscillator();
|
||
const gain = this.ctx.createGain();
|
||
osc.type = 'sine';
|
||
osc.frequency.setValueAtTime(1200, this.ctx.currentTime);
|
||
osc.frequency.exponentialRampToValueAtTime(2000, this.ctx.currentTime + 0.1);
|
||
gain.gain.setValueAtTime(0.1, this.ctx.currentTime);
|
||
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.2);
|
||
osc.connect(gain);
|
||
gain.connect(this.masterGain);
|
||
osc.start();
|
||
osc.stop(this.ctx.currentTime + 0.2);
|
||
}
|
||
}
|
||
|
||
// --- 2. 全局配置与状态 ---
|
||
const Config = {
|
||
colors: { ship: 0xffffff, engine: 0x00f3ff, laser: 0xff0055, enemy: 0xff3333, resource: 0xffcc00 },
|
||
worldSize: 2000,
|
||
};
|
||
|
||
const State = {
|
||
resources: 0,
|
||
hp: 100,
|
||
maxHp: 100,
|
||
shield: 100,
|
||
maxShield: 100,
|
||
levels: { engine: 1, shield: 1, weapon: 1 },
|
||
isPlaying: false,
|
||
isPaused: false,
|
||
scannedPlanet: null,
|
||
upgradeCosts: { engine: 100, shield: 80, weapon: 120 }
|
||
};
|
||
|
||
// --- 3. 游戏引擎核心 ---
|
||
class GameEngine {
|
||
constructor() {
|
||
// 检查 CANNON 是否加载
|
||
if (typeof CANNON === 'undefined') {
|
||
console.error("CANNON 物理库未能正确加载,请检查网络连接。");
|
||
alert("错误:物理引擎库加载失败,请刷新页面。");
|
||
return;
|
||
}
|
||
|
||
// Three.js 初始化
|
||
this.scene = new THREE.Scene();
|
||
this.scene.fog = new THREE.FogExp2(0x050510, 0.0008);
|
||
|
||
this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 5000);
|
||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
|
||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
this.renderer.shadowMap.enabled = true;
|
||
document.getElementById('canvas-container').appendChild(this.renderer.domElement);
|
||
|
||
// Cannon.js 物理世界
|
||
this.world = new CANNON.World();
|
||
this.world.gravity.set(0, 0, 0);
|
||
this.physicsMaterial = new CANNON.Material('slippery');
|
||
const contactMaterial = new CANNON.ContactMaterial(this.physicsMaterial, this.physicsMaterial, {
|
||
friction: 0.1,
|
||
restitution: 0.3
|
||
});
|
||
this.world.addContactMaterial(contactMaterial);
|
||
|
||
// 灯光
|
||
this.scene.add(new THREE.AmbientLight(0x404040, 1.2));
|
||
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||
sunLight.position.set(500, 300, 500);
|
||
sunLight.castShadow = true;
|
||
this.scene.add(sunLight);
|
||
|
||
// 实体与队列
|
||
this.entities = [];
|
||
this.particles = []; // 简单的视觉粒子
|
||
this.radarDots = [];
|
||
|
||
// 输入
|
||
this.input = { w: false, a: false, s: false, d: false, mouseX: 0, mouseY: 0, mouseDown: false };
|
||
|
||
// 工具
|
||
this.clock = new THREE.Clock();
|
||
this.raycaster = new THREE.Raycaster();
|
||
this.plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); // 用于鼠标拾取的平面
|
||
|
||
this.initListeners();
|
||
this.createEnvironment();
|
||
|
||
// 绑定动画循环
|
||
this.animate = this.animate.bind(this);
|
||
requestAnimationFrame(this.animate);
|
||
|
||
// 移除加载遮罩
|
||
setTimeout(() => {
|
||
document.getElementById('loader').style.opacity = 0;
|
||
setTimeout(() => document.getElementById('loader').style.display = 'none', 1000);
|
||
}, 1500);
|
||
}
|
||
|
||
initListeners() {
|
||
window.addEventListener('resize', () => {
|
||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||
this.camera.updateProjectionMatrix();
|
||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||
});
|
||
document.addEventListener('keydown', e => {
|
||
if(e.key.toLowerCase() === 'w') this.input.w = true;
|
||
if(e.key.toLowerCase() === 'a') this.input.a = true;
|
||
if(e.key.toLowerCase() === 's') this.input.s = true;
|
||
if(e.key.toLowerCase() === 'd') this.input.d = true;
|
||
});
|
||
document.addEventListener('keyup', e => {
|
||
if(e.key.toLowerCase() === 'w') this.input.w = false;
|
||
if(e.key.toLowerCase() === 'a') this.input.a = false;
|
||
if(e.key.toLowerCase() === 's') this.input.s = false;
|
||
if(e.key.toLowerCase() === 'd') this.input.d = false;
|
||
});
|
||
document.addEventListener('mousemove', e => {
|
||
this.input.mouseX = (e.clientX / window.innerWidth) * 2 - 1;
|
||
this.input.mouseY = -(e.clientY / window.innerHeight) * 2 + 1;
|
||
});
|
||
document.addEventListener('mousedown', () => {
|
||
this.input.mouseDown = true;
|
||
if(State.isPlaying && !State.isPaused && this.player) this.shoot(this.player, true);
|
||
});
|
||
document.addEventListener('mouseup', () => this.input.mouseDown = false);
|
||
}
|
||
|
||
createEnvironment() {
|
||
// 星空
|
||
const starGeo = new THREE.BufferGeometry();
|
||
const starCount = 3000;
|
||
const posArray = new Float32Array(starCount * 3);
|
||
for(let i=0; i<starCount*3; i++) posArray[i] = (Math.random()-0.5)*4000;
|
||
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
|
||
this.scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({size: 1.5, color: 0xffffff, transparent: true})));
|
||
|
||
// 随机生成星球
|
||
for(let i=0; i<6; i++) {
|
||
const pos = new THREE.Vector3((Math.random()-0.5)*1500, 0, (Math.random()-0.5)*1500);
|
||
if(pos.length() < 400) pos.setLength(600);
|
||
this.createPlanet(pos);
|
||
}
|
||
}
|
||
|
||
createPlanet(pos) {
|
||
const r = 80 + Math.random() * 100;
|
||
const geo = new THREE.SphereGeometry(r, 32, 32);
|
||
const color = new THREE.Color().setHSL(Math.random(), 0.8, 0.4);
|
||
const mesh = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color: color, roughness: 0.9 }));
|
||
mesh.position.copy(pos);
|
||
this.scene.add(mesh);
|
||
|
||
// 大气
|
||
const atmo = new THREE.Mesh(new THREE.SphereGeometry(r*1.2, 32, 32), new THREE.MeshBasicMaterial({color: color, transparent: true, opacity: 0.15, side: THREE.BackSide}));
|
||
mesh.add(atmo);
|
||
|
||
// 物理体
|
||
const body = new CANNON.Body({ mass: 0, shape: new CANNON.Sphere(r) });
|
||
body.position.copy(pos);
|
||
this.world.addBody(body);
|
||
|
||
this.entities.push({
|
||
type: 'planet',
|
||
mesh: mesh,
|
||
body: body,
|
||
radius: r,
|
||
data: {
|
||
name: `P-${Math.floor(Math.random()*9000)+1000}`,
|
||
temp: Math.floor(Math.random()*200-100) + "°C",
|
||
habit: Math.random() > 0.7 ? "S级 (宜居)" : "C级 (恶劣)",
|
||
resVal: Math.floor(Math.random()*300) + 100
|
||
}
|
||
});
|
||
}
|
||
|
||
createPlayer() {
|
||
const group = new THREE.Group();
|
||
// 机身
|
||
const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 4), new THREE.MeshStandardMaterial({ color: Config.colors.ship }));
|
||
group.add(body);
|
||
// 引擎
|
||
const engine = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1, 8), new THREE.MeshBasicMaterial({ color: Config.colors.engine }));
|
||
engine.rotation.x = Math.PI/2;
|
||
engine.position.set(0, 0, 2.5);
|
||
group.add(engine);
|
||
|
||
this.scene.add(group);
|
||
|
||
const physBody = new CANNON.Body({
|
||
mass: 10,
|
||
shape: new CANNON.Box(new CANNON.Vec3(1, 0.5, 2)),
|
||
linearDamping: 0.95,
|
||
angularDamping: 0.95
|
||
});
|
||
this.world.addBody(physBody);
|
||
|
||
this.player = {
|
||
type: 'player',
|
||
mesh: group,
|
||
body: physBody,
|
||
engine: this,
|
||
lastShot: 0,
|
||
throttle: 0
|
||
};
|
||
|
||
// 碰撞事件监听 (受伤)
|
||
physBody.addEventListener('collide', (e) => {
|
||
const normal = new CANNON.Vec3();
|
||
e.contact.ni.negate(normal); // 碰撞方向
|
||
const relativeVelocity = e.contact.getImpactVelocityAlongNormal();
|
||
if(Math.abs(relativeVelocity) > 5) {
|
||
Game.takeDamage(Math.floor(Math.abs(relativeVelocity) * 2));
|
||
this.createExplosion(physBody.position, 0.5);
|
||
}
|
||
});
|
||
}
|
||
|
||
spawnEnemy() {
|
||
if(this.entities.filter(e => e.type === 'enemy').length > 5) return;
|
||
|
||
const pos = this.player.mesh.position.clone().add(new THREE.Vector3((Math.random()-0.5)*300, 0, (Math.random()-0.5)*300));
|
||
const mesh = new THREE.Mesh(new THREE.ConeGeometry(3, 8, 4), new THREE.MeshStandardMaterial({ color: Config.colors.enemy }));
|
||
mesh.rotation.x = Math.PI/2;
|
||
mesh.position.copy(pos);
|
||
this.scene.add(mesh);
|
||
|
||
const body = new CANNON.Body({ mass: 5, linearDamping: 0.5 });
|
||
// 使用球体作为碰撞体
|
||
body.addShape(new CANNON.Sphere(3));
|
||
body.position.copy(pos);
|
||
this.world.addBody(body);
|
||
|
||
const enemy = { type: 'enemy', mesh: mesh, body: body, hp: 30, lastShot: 0, engine: this };
|
||
this.entities.push(enemy);
|
||
}
|
||
|
||
shoot(source, isPlayer) {
|
||
const now = Date.now();
|
||
const cd = isPlayer ? 200 - State.levels.weapon * 15 : 2000;
|
||
if (now - source.lastShot < cd) return;
|
||
source.lastShot = now;
|
||
|
||
new SoundManager().playShoot();
|
||
|
||
const direction = new THREE.Vector3();
|
||
if(isPlayer) {
|
||
direction.set(0, 0, 1).applyQuaternion(source.mesh.quaternion);
|
||
} else {
|
||
direction.subVectors(this.player.mesh.position, source.mesh.position).normalize();
|
||
}
|
||
|
||
const mesh = new THREE.Mesh(new THREE.SphereGeometry(0.5), new THREE.MeshBasicMaterial({ color: Config.colors.laser }));
|
||
mesh.position.copy(source.mesh.position).add(direction.clone().multiplyScalar(3));
|
||
this.scene.add(mesh);
|
||
|
||
const body = new CANNON.Body({ mass: 0.5, shape: new CANNON.Sphere(0.5), linearDamping: 0 });
|
||
body.position.copy(mesh.position);
|
||
body.velocity.copy(direction.multiplyScalar(isPlayer ? 300 : 100));
|
||
|
||
// 子弹碰撞逻辑:这里使用一个简单的 owner 标记
|
||
body.isBullet = true;
|
||
body.owner = isPlayer ? 'player' : 'enemy';
|
||
|
||
// 为子弹绑定碰撞事件
|
||
body.addEventListener('collide', (e) => {
|
||
// 【关键修复】使用 setTimeout 延迟移除,避免物理引擎崩溃
|
||
setTimeout(() => this.removeEntity(mesh, body), 0);
|
||
|
||
this.createExplosion(body.position, 0.3);
|
||
|
||
if (isPlayer) {
|
||
// 玩家子弹击中某物
|
||
const enemy = this.entities.find(ent => ent.body === e.body && ent.type === 'enemy');
|
||
if (enemy) {
|
||
enemy.hp -= (10 + State.levels.weapon * 5);
|
||
if (enemy.hp <= 0) {
|
||
Game.addResource(20);
|
||
// 延迟移除敌人
|
||
setTimeout(() => {
|
||
this.removeEntity(enemy.mesh, enemy.body);
|
||
this.entities = this.entities.filter(ent => ent !== enemy);
|
||
}, 0);
|
||
this.createExplosion(enemy.body.position, 2);
|
||
new SoundManager().playExplosion();
|
||
}
|
||
}
|
||
} else {
|
||
// 敌人子弹击中玩家
|
||
Game.takeDamage(10 + State.levels.weapon * 2);
|
||
}
|
||
});
|
||
|
||
this.world.addBody(body);
|
||
|
||
// 管理子弹生命周期 (临时实体)
|
||
const bullet = { type: 'bullet', mesh: mesh, body: body, life: 2000 };
|
||
this.entities.push(bullet);
|
||
}
|
||
|
||
createExplosion(pos, scale = 1) {
|
||
new SoundManager().playExplosion();
|
||
const particleCount = 8 * scale;
|
||
const geo = new THREE.BoxGeometry(scale, scale, scale);
|
||
const mat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
|
||
|
||
for(let i=0; i<particleCount; i++) {
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.copy(pos);
|
||
mesh.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
|
||
this.scene.add(mesh);
|
||
|
||
this.particles.push({
|
||
mesh: mesh,
|
||
vel: new THREE.Vector3((Math.random()-0.5)*10, (Math.random()-0.5)*10, (Math.random()-0.5)*10).multiplyScalar(scale),
|
||
life: 1.0
|
||
});
|
||
}
|
||
}
|
||
|
||
createResource(pos) {
|
||
const mesh = new THREE.Mesh(new THREE.OctahedronGeometry(2), new THREE.MeshStandardMaterial({
|
||
color: Config.colors.resource, emissive: Config.colors.resource, emissiveIntensity: 0.5
|
||
}));
|
||
mesh.position.copy(pos);
|
||
this.scene.add(mesh);
|
||
|
||
const body = new CANNON.Body({ mass: 1, shape: new CANNON.Sphere(2), isTrigger: true }); // Trigger: 不产生物理碰撞反弹
|
||
body.position.copy(pos);
|
||
|
||
// 【关键修复】资源碰撞监听
|
||
body.addEventListener('collide', (e) => {
|
||
if (e.body === this.player.body) {
|
||
Game.addResource(50);
|
||
// 延迟移除
|
||
setTimeout(() => {
|
||
this.removeEntity(mesh, body);
|
||
this.entities = this.entities.filter(ent => ent !== res);
|
||
}, 0);
|
||
new SoundManager().playCollect();
|
||
this.createExplosion(pos, 0.5);
|
||
}
|
||
});
|
||
|
||
this.world.addBody(body);
|
||
const res = { type: 'resource', mesh: mesh, body: body };
|
||
this.entities.push(res);
|
||
}
|
||
|
||
removeEntity(mesh, body) {
|
||
if(mesh && this.scene.children.includes(mesh)) this.scene.remove(mesh);
|
||
if(body && body.world === this.world) this.world.removeBody(body);
|
||
}
|
||
|
||
update(delta) {
|
||
if (!State.isPlaying || State.isPaused) return;
|
||
|
||
// 1. 物理步进
|
||
this.world.step(1/60, delta, 3);
|
||
|
||
// 2. 玩家控制
|
||
if(this.player) {
|
||
const speed = 30 * (1 + State.levels.engine * 0.1);
|
||
const rotSpeed = 2.5;
|
||
|
||
// 移动力
|
||
if(this.input.w) {
|
||
this.player.body.applyLocalForce(new CANNON.Vec3(0, 0, -speed), this.player.body.position);
|
||
if (Math.random() > 0.8) new SoundManager().playThrust();
|
||
}
|
||
if(this.input.s) this.player.body.applyLocalForce(new CANNON.Vec3(0, 0, speed), this.player.body.position);
|
||
if(this.input.a) this.player.body.angularVelocity.y += rotSpeed * delta;
|
||
if(this.input.d) this.player.body.angularVelocity.y -= rotSpeed * delta;
|
||
|
||
// 同步位置
|
||
this.player.mesh.position.copy(this.player.body.position);
|
||
this.player.mesh.quaternion.copy(this.player.body.quaternion);
|
||
|
||
// 鼠标朝向 (绕Y轴)
|
||
// 计算鼠标在3D空间的方向
|
||
this.raycaster.setFromCamera({x: this.input.mouseX, y: this.input.mouseY}, this.camera);
|
||
const target = new THREE.Vector3();
|
||
this.raycaster.ray.intersectPlane(this.plane, target);
|
||
|
||
if(target) {
|
||
const lookDir = new THREE.Vector3().subVectors(target, this.player.mesh.position).normalize();
|
||
// 我们只关心水平面旋转
|
||
lookDir.y = 0;
|
||
lookDir.normalize();
|
||
|
||
// 使用 quaternion slerp 平滑旋转
|
||
const targetQuat = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0,0,1), lookDir);
|
||
this.player.mesh.quaternion.slerp(targetQuat, delta * 5);
|
||
this.player.body.quaternion.copy(this.player.mesh.quaternion);
|
||
}
|
||
|
||
// 相机跟随
|
||
const camOffset = new THREE.Vector3(0, 30, -40).applyQuaternion(this.player.mesh.quaternion).add(this.player.mesh.position);
|
||
this.camera.position.lerp(camOffset, delta * 2);
|
||
this.camera.lookAt(this.player.mesh.position);
|
||
|
||
// 更新坐标显示
|
||
document.getElementById('coords').innerText = `${Math.floor(this.player.mesh.position.x)}, ${Math.floor(this.player.mesh.position.z)}`;
|
||
}
|
||
|
||
// 3. 实体更新
|
||
for (let i = this.entities.length - 1; i >= 0; i--) {
|
||
const ent = this.entities[i];
|
||
|
||
// 【关键修复】如果物理体已被移除(world === null),则从实体列表中删除,避免后续同步错误
|
||
if (ent.body.world === null) {
|
||
this.entities.splice(i, 1);
|
||
continue;
|
||
}
|
||
|
||
if(ent.type === 'bullet') {
|
||
ent.life -= delta * 1000;
|
||
if(ent.life <= 0) {
|
||
this.removeEntity(ent.mesh, ent.body);
|
||
this.entities.splice(i, 1);
|
||
continue;
|
||
}
|
||
}
|
||
if(ent.type === 'enemy') {
|
||
// AI 简单的追踪
|
||
const dir = new THREE.Vector3().subVectors(this.player.mesh.position, ent.mesh.position).normalize();
|
||
ent.body.velocity.copy(dir.multiplyScalar(15));
|
||
ent.mesh.position.copy(ent.body.position);
|
||
ent.mesh.lookAt(this.player.mesh.position);
|
||
|
||
// 射击
|
||
const dist = ent.mesh.position.distanceTo(this.player.mesh.position);
|
||
if (dist < 200 && Math.random() < 0.01) this.shoot(ent, false);
|
||
}
|
||
if(ent.type === 'resource' || ent.type === 'asteroid') {
|
||
ent.mesh.position.copy(ent.body.position);
|
||
ent.mesh.rotation.x += delta;
|
||
ent.mesh.rotation.y += delta;
|
||
}
|
||
}
|
||
|
||
// 4. 粒子更新
|
||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||
const p = this.particles[i];
|
||
p.life -= delta;
|
||
p.mesh.position.add(p.vel.clone().multiplyScalar(delta));
|
||
p.mesh.scale.setScalar(p.life);
|
||
p.mesh.material.opacity = p.life;
|
||
if(p.life <= 0) {
|
||
this.scene.remove(p.mesh);
|
||
this.particles.splice(i, 1);
|
||
}
|
||
}
|
||
|
||
// 5. 鼠标悬停检测 (UI提示)
|
||
this.raycaster.setFromCamera({x: this.input.mouseX, y: this.input.mouseY}, this.camera);
|
||
const intersects = this.raycaster.intersectObjects(this.entities.filter(e => e.type === 'planet').map(e => e.mesh));
|
||
const crosshair = document.getElementById('crosshair');
|
||
if(intersects.length > 0 && intersects[0].distance < 500) {
|
||
crosshair.classList.add('hovering');
|
||
// 记录当前悬停的星球用于F键交互
|
||
this.hoveredPlanet = this.entities.find(e => e.mesh === intersects[0].object);
|
||
} else {
|
||
crosshair.classList.remove('hovering');
|
||
this.hoveredPlanet = null;
|
||
}
|
||
|
||
// 6. 雷达更新
|
||
this.updateRadar();
|
||
|
||
// 7. 生成逻辑 (简单的随机)
|
||
if(Math.random() < 0.005) this.spawnAsteroid();
|
||
if(Math.random() < 0.002) this.spawnEnemy(); // 敌人生成率 - 现在这个方法名是对的了
|
||
}
|
||
|
||
spawnAsteroid() {
|
||
const pos = this.player.mesh.position.clone().add(new THREE.Vector3((Math.random()-0.5)*600, (Math.random()-0.5)*200, (Math.random()-0.5)*600));
|
||
const r = 2 + Math.random()*3;
|
||
const geo = new THREE.DodecahedronGeometry(r, 0);
|
||
const mat = new THREE.MeshStandardMaterial({ color: 0x888888, flatShading: true });
|
||
const mesh = new THREE.Mesh(geo, mat);
|
||
mesh.position.copy(pos);
|
||
this.scene.add(mesh);
|
||
|
||
const body = new CANNON.Body({ mass: r*2, shape: new CANNON.Sphere(r), linearDamping: 0.1 });
|
||
body.position.copy(pos);
|
||
body.velocity.set(Math.random(), Math.random(), Math.random());
|
||
|
||
// 碰撞
|
||
body.addEventListener('collide', (e) => {
|
||
if(e.body === this.player.body) {
|
||
Game.takeDamage(10);
|
||
// 【关键修复】延迟移除
|
||
setTimeout(() => {
|
||
this.removeEntity(mesh, body);
|
||
this.entities = this.entities.filter(ent => ent !== ast);
|
||
}, 0);
|
||
this.createExplosion(body.position, 1);
|
||
}
|
||
// 子弹碰撞
|
||
const bullet = this.entities.find(ent => ent.body === e.body && ent.type === 'bullet');
|
||
if(bullet) {
|
||
// 【关键修复】延迟移除
|
||
setTimeout(() => {
|
||
this.removeEntity(mesh, body);
|
||
this.entities = this.entities.filter(ent => ent !== ast);
|
||
}, 0);
|
||
// 掉落资源几率
|
||
if(Math.random() > 0.5) this.createResource(body.position);
|
||
}
|
||
});
|
||
|
||
this.world.addBody(body);
|
||
const ast = { type: 'asteroid', mesh: mesh, body: body };
|
||
this.entities.push(ast);
|
||
}
|
||
|
||
updateRadar() {
|
||
const radar = document.getElementById('radar-display');
|
||
// 清理旧的点
|
||
this.radarDots.forEach(d => d.remove());
|
||
this.radarDots = [];
|
||
|
||
if(!this.player) return;
|
||
|
||
this.entities.forEach(ent => {
|
||
if(ent.type === 'player') return;
|
||
// 计算相对位置
|
||
const dx = ent.mesh.position.x - this.player.mesh.position.x;
|
||
const dz = ent.mesh.position.z - this.player.mesh.position.z;
|
||
const dist = Math.sqrt(dx*dx + dz*dz);
|
||
|
||
if(dist < 400) { // 雷达范围
|
||
const dot = document.createElement('div');
|
||
dot.style.position = 'absolute';
|
||
// 映射到雷达像素: 半径60px
|
||
const rx = (dx / 400) * 60;
|
||
const ry = (dz / 400) * 60;
|
||
dot.style.left = (60 + rx) + 'px';
|
||
dot.style.top = (60 + ry) + 'px';
|
||
dot.style.width = '4px';
|
||
dot.style.height = '4px';
|
||
dot.style.borderRadius = '50%';
|
||
|
||
if(ent.type === 'enemy') dot.style.background = '#ff0055';
|
||
else if(ent.type === 'planet') dot.style.background = '#00f3ff';
|
||
else if(ent.type === 'resource') dot.style.background = '#ffcc00';
|
||
|
||
radar.appendChild(dot);
|
||
this.radarDots.push(dot);
|
||
}
|
||
});
|
||
}
|
||
|
||
animate() {
|
||
requestAnimationFrame(this.animate);
|
||
const delta = this.clock.getDelta();
|
||
this.update(delta);
|
||
this.renderer.render(this.scene, this.camera);
|
||
}
|
||
}
|
||
|
||
// --- 4. 游戏逻辑控制器 ---
|
||
const Game = {
|
||
engine: null,
|
||
|
||
init: function() {
|
||
this.engine = new GameEngine();
|
||
this.updateUI();
|
||
},
|
||
|
||
start: function() {
|
||
document.getElementById('start-screen').classList.remove('active');
|
||
State.isPlaying = true;
|
||
this.engine.createPlayer();
|
||
new SoundManager().playThrust(); // 启动音效
|
||
},
|
||
|
||
takeDamage: function(amount) {
|
||
// 护盾抵扣
|
||
if(State.shield > 0) {
|
||
State.shield -= amount;
|
||
if(State.shield < 0) {
|
||
const rem = Math.abs(State.shield);
|
||
State.shield = 0;
|
||
State.hp -= rem;
|
||
}
|
||
} else {
|
||
State.hp -= amount;
|
||
}
|
||
|
||
this.updateUI();
|
||
|
||
if(State.hp <= 0) {
|
||
alert("飞船损毁! 任务失败。");
|
||
location.reload();
|
||
}
|
||
},
|
||
|
||
addResource: function(amount) {
|
||
State.resources += amount;
|
||
this.updateUI();
|
||
this.showFloatingText(`+${amount} RESOURCE`, '#ffcc00');
|
||
},
|
||
|
||
updateUI: function() {
|
||
document.getElementById('hp-val').innerText = Math.floor(State.hp) + '%';
|
||
document.getElementById('hp-bar').style.width = (State.hp / State.maxHp * 100) + '%';
|
||
|
||
document.getElementById('shield-val').innerText = Math.floor(State.shield) + '%';
|
||
document.getElementById('shield-bar').style.width = (State.shield / State.maxShield * 100) + '%';
|
||
|
||
document.getElementById('res-count').innerText = State.resources;
|
||
|
||
// 更新升级菜单中的资源
|
||
document.getElementById('upgrade-res').innerText = State.resources;
|
||
|
||
// 更新等级显示
|
||
document.getElementById('lvl-engine').innerText = State.levels.engine;
|
||
document.getElementById('lvl-shield').innerText = State.levels.shield;
|
||
document.getElementById('lvl-weapon').innerText = State.levels.weapon;
|
||
},
|
||
|
||
showFloatingText: function(text, color) {
|
||
const el = document.createElement('div');
|
||
el.innerText = text;
|
||
el.style.position = 'absolute';
|
||
el.style.left = '50%';
|
||
el.style.top = '40%';
|
||
el.style.transform = 'translate(-50%, -50%)';
|
||
el.style.color = color;
|
||
el.style.fontSize = '20px';
|
||
el.style.fontWeight = 'bold';
|
||
el.style.textShadow = '0 0 5px black';
|
||
el.style.pointerEvents = 'none';
|
||
el.style.transition = 'all 1s';
|
||
document.body.appendChild(el);
|
||
|
||
// 动画
|
||
setTimeout(() => {
|
||
el.style.top = '30%';
|
||
el.style.opacity = 0;
|
||
}, 50);
|
||
setTimeout(() => el.remove(), 1000);
|
||
},
|
||
|
||
tryInteract: function() {
|
||
if(this.engine.hoveredPlanet && this.engine.hoveredPlanet.type === 'planet') {
|
||
State.isPaused = true;
|
||
State.scannedPlanet = this.engine.hoveredPlanet;
|
||
|
||
// 填充数据
|
||
const p = State.scannedPlanet.data;
|
||
document.getElementById('scan-name').innerText = p.name;
|
||
document.getElementById('scan-atmo').innerText = "N2 78%, O2 21%";
|
||
document.getElementById('scan-temp').innerText = p.temp;
|
||
document.getElementById('scan-habit').innerText = p.habit;
|
||
document.getElementById('scan-res').innerText = "价值: " + p.resVal;
|
||
|
||
document.getElementById('scan-modal').classList.add('active');
|
||
}
|
||
},
|
||
|
||
closeScanModal: function() {
|
||
document.getElementById('scan-modal').classList.remove('active');
|
||
State.isPaused = false;
|
||
State.scannedPlanet = null;
|
||
},
|
||
|
||
collectPlanetResources: function() {
|
||
if(State.scannedPlanet) {
|
||
const val = State.scannedPlanet.data.resVal;
|
||
this.addResource(val);
|
||
this.closeScanModal();
|
||
this.showFloatingText("采集成功!", "#00f3ff");
|
||
new SoundManager().playCollect();
|
||
}
|
||
},
|
||
|
||
openUpgradeMenu: function() {
|
||
State.isPaused = true;
|
||
document.getElementById('upgrade-modal').classList.add('active');
|
||
},
|
||
|
||
closeUpgradeMenu: function() {
|
||
document.getElementById('upgrade-modal').classList.remove('active');
|
||
State.isPaused = false;
|
||
},
|
||
|
||
upgradeShip: function(type) {
|
||
const cost = State.upgradeCosts[type] * State.levels[type];
|
||
if(State.resources >= cost) {
|
||
State.resources -= cost;
|
||
State.levels[type]++;
|
||
|
||
// 应用即时效果
|
||
if(type === 'shield') {
|
||
State.maxShield += 50;
|
||
State.shield = State.maxShield;
|
||
}
|
||
|
||
this.updateUI();
|
||
new SoundManager().playCollect(); // 使用收集音效作为确认音
|
||
} else {
|
||
const btn = event.currentTarget;
|
||
btn.style.borderColor = 'red';
|
||
setTimeout(() => btn.style.borderColor = 'transparent', 500);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 启动初始化
|
||
window.onload = () => Game.init();
|
||
|
||
</script>
|
||
</body>
|
||
</html> |