LLM-test/test/code/3/glm-4.7.html

1248 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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