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>
|