1927 lines
70 KiB
HTML
1927 lines
70 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>星际文明探索者</title>
|
|||
|
|
<style>
|
|||
|
|
* {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@font-face {
|
|||
|
|
font-family: 'Tech';
|
|||
|
|
src: local('Orbitron'), local('Rajdhani'), local('Segoe UI');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: #000;
|
|||
|
|
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#game-container {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#game-canvas {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Glassmorphism UI Panels */
|
|||
|
|
.glass-panel {
|
|||
|
|
background: rgba(15, 20, 40, 0.7);
|
|||
|
|
backdrop-filter: blur(12px);
|
|||
|
|
-webkit-backdrop-filter: blur(12px);
|
|||
|
|
border: 1px solid rgba(100, 150, 255, 0.3);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 0 30px rgba(50, 100, 200, 0.2),
|
|||
|
|
inset 0 0 20px rgba(100, 150, 255, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* HUD Elements */
|
|||
|
|
#hud-top {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
display: flex;
|
|||
|
|
gap: 30px;
|
|||
|
|
padding: 15px 30px;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hud-item {
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hud-label {
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: rgba(150, 180, 255, 0.7);
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
letter-spacing: 2px;
|
|||
|
|
margin-bottom: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.hud-value {
|
|||
|
|
font-size: 24px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
background: linear-gradient(135deg, #00d4ff, #7b68ee);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Shield Bar */
|
|||
|
|
#shield-bar {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 30px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
width: 300px;
|
|||
|
|
height: 20px;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid rgba(100, 150, 255, 0.3);
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#shield-fill {
|
|||
|
|
height: 100%;
|
|||
|
|
width: 100%;
|
|||
|
|
background: linear-gradient(90deg, #00d4ff, #7b68ee);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
transition: width 0.3s ease;
|
|||
|
|
box-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#shield-text {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 100%;
|
|||
|
|
text-align: center;
|
|||
|
|
line-height: 20px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
text-shadow: 0 0 10px rgba(0, 212, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Resource Display */
|
|||
|
|
#resource-panel {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 80px;
|
|||
|
|
right: 20px;
|
|||
|
|
padding: 20px;
|
|||
|
|
min-width: 180px;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.resource-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.resource-icon {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.resource-icon.mineral { background: linear-gradient(135deg, #ff6b6b, #ffa500); }
|
|||
|
|
.resource-icon.crystal { background: linear-gradient(135deg, #00d4ff, #7b68ee); }
|
|||
|
|
.resource-icon.artifact { background: linear-gradient(135deg, #ffd700, #ff69b4); }
|
|||
|
|
|
|||
|
|
.resource-name {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: rgba(200, 220, 255, 0.7);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.resource-count {
|
|||
|
|
margin-left: auto;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Upgrade Panel */
|
|||
|
|
#upgrade-panel {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 50%;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
padding: 30px;
|
|||
|
|
display: none;
|
|||
|
|
z-index: 200;
|
|||
|
|
max-width: 500px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#upgrade-panel.active {
|
|||
|
|
display: block;
|
|||
|
|
animation: fadeIn 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fadeIn {
|
|||
|
|
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
|
|||
|
|
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-title {
|
|||
|
|
font-size: 28px;
|
|||
|
|
text-align: center;
|
|||
|
|
margin-bottom: 25px;
|
|||
|
|
background: linear-gradient(135deg, #00d4ff, #7b68ee);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-option {
|
|||
|
|
background: rgba(30, 40, 70, 0.8);
|
|||
|
|
border: 1px solid rgba(100, 150, 255, 0.3);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
padding: 20px;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-option:hover {
|
|||
|
|
border-color: rgba(0, 212, 255, 0.8);
|
|||
|
|
background: rgba(50, 70, 120, 0.8);
|
|||
|
|
transform: translateX(5px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-name {
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
color: #00d4ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-desc {
|
|||
|
|
font-size: 13px;
|
|||
|
|
color: rgba(200, 220, 255, 0.7);
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upgrade-level {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #7b68ee;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Scan Panel */
|
|||
|
|
#scan-panel {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 70px;
|
|||
|
|
left: 20px;
|
|||
|
|
padding: 20px;
|
|||
|
|
max-width: 300px;
|
|||
|
|
display: none;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#scan-panel.active {
|
|||
|
|
display: block;
|
|||
|
|
animation: slideIn 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes slideIn {
|
|||
|
|
from { opacity: 0; transform: translateX(-20px); }
|
|||
|
|
to { opacity: 1; transform: translateX(0); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scan-title {
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
color: #00d4ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.scan-data {
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
color: rgba(200, 220, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Minimap */
|
|||
|
|
#minimap {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
width: 150px;
|
|||
|
|
height: 150px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 2px solid rgba(100, 150, 255, 0.5);
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#minimap-canvas {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Start Screen */
|
|||
|
|
#start-screen {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a3a 50%, #0a0a2a 100%);
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#start-screen.hidden {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-title {
|
|||
|
|
font-size: 56px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
background: linear-gradient(135deg, #00d4ff, #7b68ee, #ff69b4);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
text-shadow: 0 0 50px rgba(0, 212, 255, 0.5);
|
|||
|
|
animation: titleGlow 2s ease-in-out infinite alternate;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes titleGlow {
|
|||
|
|
from { filter: drop-shadow(0 0 20px rgba(0, 212, 255, 0.5)); }
|
|||
|
|
to { filter: drop-shadow(0 0 40px rgba(123, 104, 238, 0.8)); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.game-subtitle {
|
|||
|
|
font-size: 18px;
|
|||
|
|
color: rgba(200, 220, 255, 0.6);
|
|||
|
|
margin-bottom: 50px;
|
|||
|
|
letter-spacing: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.start-btn {
|
|||
|
|
padding: 15px 60px;
|
|||
|
|
font-size: 20px;
|
|||
|
|
background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(123, 104, 238, 0.2));
|
|||
|
|
border: 2px solid rgba(0, 212, 255, 0.5);
|
|||
|
|
border-radius: 30px;
|
|||
|
|
color: #fff;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.start-btn:hover {
|
|||
|
|
background: linear-gradient(135deg, rgba(0, 212, 255, 0.4), rgba(123, 104, 238, 0.4));
|
|||
|
|
border-color: rgba(0, 212, 255, 0.8);
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
box-shadow: 0 0 30px rgba(0, 212, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.controls-info {
|
|||
|
|
margin-top: 50px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: rgba(200, 220, 255, 0.5);
|
|||
|
|
text-align: center;
|
|||
|
|
line-height: 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Crosshair */
|
|||
|
|
#crosshair {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 50%;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
pointer-events: none;
|
|||
|
|
z-index: 50;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crosshair-line {
|
|||
|
|
position: absolute;
|
|||
|
|
background: rgba(0, 212, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crosshair-line.horizontal {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 2px;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crosshair-line.left { left: -30px; }
|
|||
|
|
.crosshair-line.right { right: -30px; }
|
|||
|
|
|
|||
|
|
.crosshair-line.vertical {
|
|||
|
|
width: 2px;
|
|||
|
|
height: 20px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.crosshair-line.top { top: -30px; }
|
|||
|
|
.crosshair-line.bottom { bottom: -30px; }
|
|||
|
|
|
|||
|
|
/* Notification */
|
|||
|
|
#notification {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 100px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
padding: 15px 30px;
|
|||
|
|
background: rgba(30, 40, 70, 0.9);
|
|||
|
|
border: 1px solid rgba(0, 212, 255, 0.5);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #00d4ff;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.3s ease;
|
|||
|
|
z-index: 300;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#notification.show {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Pause Menu */
|
|||
|
|
#pause-menu {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: rgba(0, 0, 0, 0.8);
|
|||
|
|
display: none;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
z-index: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#pause-menu.active {
|
|||
|
|
display: flex;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pause-content {
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pause-title {
|
|||
|
|
font-size: 48px;
|
|||
|
|
margin-bottom: 40px;
|
|||
|
|
background: linear-gradient(135deg, #00d4ff, #7b68ee);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pause-btn {
|
|||
|
|
display: block;
|
|||
|
|
width: 200px;
|
|||
|
|
margin: 15px auto;
|
|||
|
|
padding: 12px;
|
|||
|
|
font-size: 18px;
|
|||
|
|
background: rgba(30, 40, 70, 0.8);
|
|||
|
|
border: 1px solid rgba(100, 150, 255, 0.3);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
color: #fff;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pause-btn:hover {
|
|||
|
|
background: rgba(50, 70, 120, 0.8);
|
|||
|
|
border-color: rgba(0, 212, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Combat Indicator */
|
|||
|
|
#combat-indicator {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 50%;
|
|||
|
|
right: 50px;
|
|||
|
|
transform: translateY(-50%);
|
|||
|
|
padding: 10px 20px;
|
|||
|
|
background: rgba(255, 50, 50, 0.2);
|
|||
|
|
border: 1px solid rgba(255, 50, 50, 0.5);
|
|||
|
|
border-radius: 5px;
|
|||
|
|
color: #ff4444;
|
|||
|
|
font-size: 14px;
|
|||
|
|
display: none;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#combat-indicator.active {
|
|||
|
|
display: block;
|
|||
|
|
animation: pulse 0.5s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0%, 100% { opacity: 1; }
|
|||
|
|
50% { opacity: 0.5; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Level Up Animation */
|
|||
|
|
#levelup-overlay {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: radial-gradient(circle, rgba(123, 104, 238, 0.3), transparent);
|
|||
|
|
display: none;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
z-index: 400;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#levelup-overlay.active {
|
|||
|
|
display: flex;
|
|||
|
|
animation: levelupFade 0.5s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes levelupFade {
|
|||
|
|
from { opacity: 0; }
|
|||
|
|
to { opacity: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.levelup-text {
|
|||
|
|
font-size: 72px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
background: linear-gradient(135deg, #ffd700, #ff6b6b);
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
background-clip: text;
|
|||
|
|
animation: levelupBounce 0.5s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes levelupBounce {
|
|||
|
|
0% { transform: scale(0.5); opacity: 0; }
|
|||
|
|
50% { transform: scale(1.2); }
|
|||
|
|
100% { transform: scale(1); opacity: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Loading Screen */
|
|||
|
|
#loading-screen {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: #0a0a1a;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
z-index: 2000;
|
|||
|
|
transition: opacity 0.5s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#loading-screen.hidden {
|
|||
|
|
opacity: 0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-spinner {
|
|||
|
|
width: 60px;
|
|||
|
|
height: 60px;
|
|||
|
|
border: 3px solid rgba(100, 150, 255, 0.2);
|
|||
|
|
border-top-color: #00d4ff;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
to { transform: rotate(360deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.loading-text {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: rgba(200, 220, 255, 0.7);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* XP Bar */
|
|||
|
|
#xp-bar {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 60px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
width: 400px;
|
|||
|
|
height: 8px;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid rgba(100, 150, 255, 0.2);
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#xp-fill {
|
|||
|
|
height: 100%;
|
|||
|
|
width: 0%;
|
|||
|
|
background: linear-gradient(90deg, #ffd700, #ff6b6b);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
transition: width 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#xp-text {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 100%;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 10px;
|
|||
|
|
line-height: 8px;
|
|||
|
|
color: rgba(255, 255, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Help Panel */
|
|||
|
|
#help-panel {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 50%;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
padding: 30px;
|
|||
|
|
max-width: 600px;
|
|||
|
|
display: none;
|
|||
|
|
z-index: 200;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#help-panel.active {
|
|||
|
|
display: block;
|
|||
|
|
animation: fadeIn 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.help-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 15px;
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.help-item {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10px;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.help-key {
|
|||
|
|
background: rgba(0, 212, 255, 0.2);
|
|||
|
|
border: 1px solid rgba(0, 212, 255, 0.5);
|
|||
|
|
padding: 5px 10px;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
font-family: monospace;
|
|||
|
|
min-width: 80px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<!-- Loading Screen -->
|
|||
|
|
<div id="loading-screen">
|
|||
|
|
<div class="loading-spinner"></div>
|
|||
|
|
<div class="loading-text">正在初始化星际引擎...</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Start Screen -->
|
|||
|
|
<div id="start-screen">
|
|||
|
|
<h1 class="game-title">星际文明探索者</h1>
|
|||
|
|
<p class="game-subtitle">STELLAR CIVILIZATION EXPLORER</p>
|
|||
|
|
<button class="start-btn" onclick="game.start()">开始探索</button>
|
|||
|
|
<div class="controls-info">
|
|||
|
|
<p>WASD - 移动飞船 | 鼠标 - 瞄准 | 左键 - 射击</p>
|
|||
|
|
<p>空格 - 护盾 | E - 扫描星球 | Q - 特殊技能 | ESC - 暂停</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Game Container -->
|
|||
|
|
<div id="game-container">
|
|||
|
|
<canvas id="game-canvas"></canvas>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- HUD Top -->
|
|||
|
|
<div id="hud-top" class="glass-panel" style="display: none;">
|
|||
|
|
<div class="hud-item">
|
|||
|
|
<div class="hud-label">等级</div>
|
|||
|
|
<div class="hud-value" id="level-display">1</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="hud-item">
|
|||
|
|
<div class="hud-label">分数</div>
|
|||
|
|
<div class="hud-value" id="score-display">0</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="hud-item">
|
|||
|
|
<div class="hud-label">敌人</div>
|
|||
|
|
<div class="hud-value" id="enemy-display">0</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Shield Bar -->
|
|||
|
|
<div id="shield-bar" style="display: none;">
|
|||
|
|
<div id="shield-fill"></div>
|
|||
|
|
<div id="shield-text">护盾 100%</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- XP Bar -->
|
|||
|
|
<div id="xp-bar" style="display: none;">
|
|||
|
|
<div id="xp-fill"></div>
|
|||
|
|
<div id="xp-text">0 / 100 XP</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Resource Panel -->
|
|||
|
|
<div id="resource-panel" class="glass-panel" style="display: none;">
|
|||
|
|
<div class="resource-item">
|
|||
|
|
<div class="resource-icon mineral"></div>
|
|||
|
|
<span class="resource-name">矿物</span>
|
|||
|
|
<span class="resource-count" id="mineral-count">0</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="resource-item">
|
|||
|
|
<div class="resource-icon crystal"></div>
|
|||
|
|
<span class="resource-name">能量晶体</span>
|
|||
|
|
<span class="resource-count" id="crystal-count">0</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="resource-item">
|
|||
|
|
<div class="resource-icon artifact"></div>
|
|||
|
|
<span class="resource-name">遗迹碎片</span>
|
|||
|
|
<span class="resource-count" id="artifact-count">0</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Minimap -->
|
|||
|
|
<div id="minimap" class="glass-panel" style="display: none;">
|
|||
|
|
<canvas id="minimap-canvas"></canvas>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Crosshair -->
|
|||
|
|
<div id="crosshair" style="display: none;">
|
|||
|
|
<div class="crosshair-line horizontal left"></div>
|
|||
|
|
<div class="crosshair-line horizontal right"></div>
|
|||
|
|
<div class="crosshair-line vertical top"></div>
|
|||
|
|
<div class="crosshair-line vertical bottom"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Combat Indicator -->
|
|||
|
|
<div id="combat-indicator">⚠ 战斗进行中</div>
|
|||
|
|
|
|||
|
|
<!-- Scan Panel -->
|
|||
|
|
<div id="scan-panel" class="glass-panel">
|
|||
|
|
<div class="scan-title" id="scan-name">扫描数据</div>
|
|||
|
|
<div class="scan-data" id="scan-data"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Upgrade Panel -->
|
|||
|
|
<div id="upgrade-panel" class="glass-panel">
|
|||
|
|
<div class="upgrade-title">系统升级</div>
|
|||
|
|
<div class="upgrade-option" onclick="game.selectUpgrade('engine')">
|
|||
|
|
<div class="upgrade-name">推进系统</div>
|
|||
|
|
<div class="upgrade-desc">提升移动速度和加速度</div>
|
|||
|
|
<div class="upgrade-level" id="engine-level">当前等级: 1</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="upgrade-option" onclick="game.selectUpgrade('shield')">
|
|||
|
|
<div class="upgrade-name">能量护盾</div>
|
|||
|
|
<div class="upgrade-desc">增加护盾容量和恢复速度</div>
|
|||
|
|
<div class="upgrade-level" id="shield-level">当前等级: 1</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="upgrade-option" onclick="game.selectUpgrade('weapon')">
|
|||
|
|
<div class="upgrade-name">武器系统</div>
|
|||
|
|
<div class="upgrade-desc">提升武器威力和射速</div>
|
|||
|
|
<div class="upgrade-level" id="weapon-level">当前等级: 1</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="upgrade-option" onclick="game.selectUpgrade('scanner')">
|
|||
|
|
<div class="upgrade-name">扫描仪</div>
|
|||
|
|
<div class="upgrade-desc">增加资源发现概率和信息完整度</div>
|
|||
|
|
<div class="upgrade-level" id="scanner-level">当前等级: 1</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Notification -->
|
|||
|
|
<div id="notification"></div>
|
|||
|
|
|
|||
|
|
<!-- Pause Menu -->
|
|||
|
|
<div id="pause-menu">
|
|||
|
|
<div class="pause-content glass-panel" style="padding: 40px;">
|
|||
|
|
<div class="pause-title">暂停</div>
|
|||
|
|
<button class="pause-btn" onclick="game.resume()">继续游戏</button>
|
|||
|
|
<button class="pause-btn" onclick="game.showHelp()">操作说明</button>
|
|||
|
|
<button class="pause-btn" onclick="game.saveGame()">保存进度</button>
|
|||
|
|
<button class="pause-btn" onclick="game.loadGame()">读取存档</button>
|
|||
|
|
<button class="pause-btn" onclick="game.returnToMenu()">返回主菜单</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Help Panel -->
|
|||
|
|
<div id="help-panel" class="glass-panel">
|
|||
|
|
<div class="upgrade-title">操作说明</div>
|
|||
|
|
<div class="help-grid">
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">WASD</span>
|
|||
|
|
<span>移动飞船</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">鼠标</span>
|
|||
|
|
<span>瞄准方向</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">左键</span>
|
|||
|
|
<span>发射武器</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">空格</span>
|
|||
|
|
<span>激活护盾</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">E</span>
|
|||
|
|
<span>扫描附近星球</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">Q</span>
|
|||
|
|
<span>释放特殊技能</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="help-item">
|
|||
|
|
<span class="help-key">ESC</span>
|
|||
|
|
<span>暂停菜单</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<button class="pause-btn" style="margin-top: 20px;" onclick="game.hideHelp()">返回游戏</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Level Up Overlay -->
|
|||
|
|
<div id="levelup-overlay">
|
|||
|
|
<div class="levelup-text">LEVEL UP!</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- CDN Libraries -->
|
|||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
|
|||
|
|
<script src="https://cdn.bootcdn.net/ajax/libs/howler/2.2.4/howler.min.js"></script>
|
|||
|
|
<script src="https://cdn.bootcdn.net/ajax/libs/tsparticles/3.9.1/tsparticles.min.js"></script>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// ==================== Audio System ====================
|
|||
|
|
class AudioSystem {
|
|||
|
|
constructor() {
|
|||
|
|
this.sounds = {};
|
|||
|
|
this.bgmVolume = 0.3;
|
|||
|
|
this.sfxVolume = 0.5;
|
|||
|
|
this.initialized = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
// Create audio context for synthesized sounds
|
|||
|
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|||
|
|
this.initialized = true;
|
|||
|
|
console.log('音频系统已初始化');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
playShoot() {
|
|||
|
|
if (!this.initialized) return;
|
|||
|
|
const osc = this.audioContext.createOscillator();
|
|||
|
|
const gain = this.audioContext.createGain();
|
|||
|
|
osc.connect(gain);
|
|||
|
|
gain.connect(this.audioContext.destination);
|
|||
|
|
osc.frequency.setValueAtTime(880, this.audioContext.currentTime);
|
|||
|
|
osc.frequency.exponentialRampToValueAtTime(110, this.audioContext.currentTime + 0.1);
|
|||
|
|
gain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|||
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.1);
|
|||
|
|
osc.start();
|
|||
|
|
osc.stop(this.audioContext.currentTime + 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
playExplosion() {
|
|||
|
|
if (!this.initialized) return;
|
|||
|
|
const bufferSize = this.audioContext.sampleRate * 0.3;
|
|||
|
|
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
|
|||
|
|
const data = buffer.getChannelData(0);
|
|||
|
|
for (let i = 0; i < bufferSize; i++) {
|
|||
|
|
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / bufferSize, 2);
|
|||
|
|
}
|
|||
|
|
const source = this.audioContext.createBufferSource();
|
|||
|
|
const gain = this.audioContext.createGain();
|
|||
|
|
source.buffer = buffer;
|
|||
|
|
source.connect(gain);
|
|||
|
|
gain.connect(this.audioContext.destination);
|
|||
|
|
gain.gain.setValueAtTime(0.5, this.audioContext.currentTime);
|
|||
|
|
source.start();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
playCollect() {
|
|||
|
|
if (!this.initialized) return;
|
|||
|
|
const osc = this.audioContext.createOscillator();
|
|||
|
|
const gain = this.audioContext.createGain();
|
|||
|
|
osc.connect(gain);
|
|||
|
|
gain.connect(this.audioContext.destination);
|
|||
|
|
osc.frequency.setValueAtTime(523, this.audioContext.currentTime);
|
|||
|
|
osc.frequency.setValueAtTime(659, this.audioContext.currentTime + 0.1);
|
|||
|
|
osc.frequency.setValueAtTime(784, this.audioContext.currentTime + 0.2);
|
|||
|
|
gain.gain.setValueAtTime(0.2, this.audioContext.currentTime);
|
|||
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.3);
|
|||
|
|
osc.start();
|
|||
|
|
osc.stop(this.audioContext.currentTime + 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
playLevelUp() {
|
|||
|
|
if (!this.initialized) return;
|
|||
|
|
const notes = [523, 659, 784, 1047];
|
|||
|
|
notes.forEach((freq, i) => {
|
|||
|
|
const osc = this.audioContext.createOscillator();
|
|||
|
|
const gain = this.audioContext.createGain();
|
|||
|
|
osc.connect(gain);
|
|||
|
|
gain.connect(this.audioContext.destination);
|
|||
|
|
osc.frequency.setValueAtTime(freq, this.audioContext.currentTime + i * 0.1);
|
|||
|
|
gain.gain.setValueAtTime(0.3, this.audioContext.currentTime + i * 0.1);
|
|||
|
|
gain.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + i * 0.1 + 0.3);
|
|||
|
|
osc.start(this.audioContext.currentTime + i * 0.1);
|
|||
|
|
osc.stop(this.audioContext.currentTime + i * 0.1 + 0.3);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Particle System ====================
|
|||
|
|
class ParticleSystem {
|
|||
|
|
constructor(scene) {
|
|||
|
|
this.scene = scene;
|
|||
|
|
this.particles = [];
|
|||
|
|
this.maxParticles = 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createEngineTrail(position, direction) {
|
|||
|
|
for (let i = 0; i < 3; i++) {
|
|||
|
|
const geometry = new THREE.SphereGeometry(0.1, 8, 8);
|
|||
|
|
const material = new THREE.MeshBasicMaterial({
|
|||
|
|
color: new THREE.Color().setHSL(0.1 + Math.random() * 0.1, 1, 0.5),
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.8
|
|||
|
|
});
|
|||
|
|
const particle = new THREE.Mesh(geometry, material);
|
|||
|
|
particle.position.copy(position);
|
|||
|
|
particle.velocity = new THREE.Vector3(
|
|||
|
|
-direction.x * 2 + (Math.random() - 0.5),
|
|||
|
|
-direction.y * 2 + (Math.random() - 0.5),
|
|||
|
|
-direction.z * 2 + (Math.random() - 0.5)
|
|||
|
|
);
|
|||
|
|
particle.life = 1.0;
|
|||
|
|
particle.decay = 0.02 + Math.random() * 0.02;
|
|||
|
|
this.scene.add(particle);
|
|||
|
|
this.particles.push(particle);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createExplosion(position, color = 0xff6600) {
|
|||
|
|
const count = 30;
|
|||
|
|
for (let i = 0; i < count; i++) {
|
|||
|
|
const geometry = new THREE.SphereGeometry(0.15, 6, 6);
|
|||
|
|
const material = new THREE.MeshBasicMaterial({
|
|||
|
|
color: color,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 1
|
|||
|
|
});
|
|||
|
|
const particle = new THREE.Mesh(geometry, material);
|
|||
|
|
particle.position.copy(position);
|
|||
|
|
const theta = Math.random() * Math.PI * 2;
|
|||
|
|
const phi = Math.random() * Math.PI;
|
|||
|
|
const speed = 2 + Math.random() * 3;
|
|||
|
|
particle.velocity = new THREE.Vector3(
|
|||
|
|
Math.sin(phi) * Math.cos(theta) * speed,
|
|||
|
|
Math.sin(phi) * Math.sin(theta) * speed,
|
|||
|
|
Math.cos(phi) * speed
|
|||
|
|
);
|
|||
|
|
particle.life = 1.0;
|
|||
|
|
particle.decay = 0.015 + Math.random() * 0.01;
|
|||
|
|
this.scene.add(particle);
|
|||
|
|
this.particles.push(particle);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createLaser(start, end, color = 0x00ffff) {
|
|||
|
|
const geometry = new THREE.BufferGeometry().setFromPoints([start, end]);
|
|||
|
|
const material = new THREE.LineBasicMaterial({
|
|||
|
|
color: color,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.8
|
|||
|
|
});
|
|||
|
|
const line = new THREE.Line(geometry, material);
|
|||
|
|
line.life = 0.1;
|
|||
|
|
line.decay = 0.1;
|
|||
|
|
this.scene.add(line);
|
|||
|
|
this.particles.push(line);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update() {
|
|||
|
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|||
|
|
const p = this.particles[i];
|
|||
|
|
p.life -= p.decay;
|
|||
|
|
if (p.life <= 0) {
|
|||
|
|
this.scene.remove(p);
|
|||
|
|
this.particles.splice(i, 1);
|
|||
|
|
} else {
|
|||
|
|
if (p.velocity) {
|
|||
|
|
p.position.add(p.velocity.clone().multiplyScalar(0.1));
|
|||
|
|
}
|
|||
|
|
if (p.material) {
|
|||
|
|
p.material.opacity = p.life * 0.8;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Spaceship ====================
|
|||
|
|
class Spaceship {
|
|||
|
|
constructor(scene) {
|
|||
|
|
this.scene = scene;
|
|||
|
|
this.group = new THREE.Group();
|
|||
|
|
this.velocity = new THREE.Vector3();
|
|||
|
|
this.acceleration = new THREE.Vector3();
|
|||
|
|
this.rotationVelocity = new THREE.Vector2();
|
|||
|
|
this.createModel();
|
|||
|
|
this.group.position.set(0, 0, 0);
|
|||
|
|
scene.add(this.group);
|
|||
|
|
|
|||
|
|
// Stats
|
|||
|
|
this.maxShield = 100;
|
|||
|
|
this.shield = 100;
|
|||
|
|
this.speed = 0.5;
|
|||
|
|
this.turnSpeed = 0.03;
|
|||
|
|
this.weaponLevel = 1;
|
|||
|
|
this.engineLevel = 1;
|
|||
|
|
this.shieldLevel = 1;
|
|||
|
|
this.scannerLevel = 1;
|
|||
|
|
|
|||
|
|
// State
|
|||
|
|
this.shieldActive = false;
|
|||
|
|
this.shieldRegenCooldown = 0;
|
|||
|
|
this.lastShot = 0;
|
|||
|
|
this.fireRate = 150;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createModel() {
|
|||
|
|
// Main body
|
|||
|
|
const bodyGeo = new THREE.ConeGeometry(0.5, 2, 8);
|
|||
|
|
const bodyMat = new THREE.MeshPhongMaterial({
|
|||
|
|
color: 0x3366ff,
|
|||
|
|
emissive: 0x112244,
|
|||
|
|
shininess: 100
|
|||
|
|
});
|
|||
|
|
const body = new THREE.Mesh(bodyGeo, bodyMat);
|
|||
|
|
body.rotation.x = Math.PI / 2;
|
|||
|
|
this.group.add(body);
|
|||
|
|
|
|||
|
|
// Wings
|
|||
|
|
const wingGeo = new THREE.BoxGeometry(2.5, 0.1, 0.8);
|
|||
|
|
const wingMat = new THREE.MeshPhongMaterial({
|
|||
|
|
color: 0x2255cc,
|
|||
|
|
emissive: 0x111133
|
|||
|
|
});
|
|||
|
|
const wings = new THREE.Mesh(wingGeo, wingMat);
|
|||
|
|
wings.position.z = 0.3;
|
|||
|
|
this.group.add(wings);
|
|||
|
|
|
|||
|
|
// Cockpit
|
|||
|
|
const cockpitGeo = new THREE.SphereGeometry(0.3, 16, 16);
|
|||
|
|
const cockpitMat = new THREE.MeshPhongMaterial({
|
|||
|
|
color: 0x00ffff,
|
|||
|
|
emissive: 0x00aaaa,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.8
|
|||
|
|
});
|
|||
|
|
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
|
|||
|
|
cockpit.position.set(0, 0.2, -0.3);
|
|||
|
|
cockpit.scale.set(1, 0.6, 1.5);
|
|||
|
|
this.group.add(cockpit);
|
|||
|
|
|
|||
|
|
// Engine glow
|
|||
|
|
const engineGeo = new THREE.CylinderGeometry(0.2, 0.3, 0.3, 16);
|
|||
|
|
const engineMat = new THREE.MeshBasicMaterial({ color: 0x00aaff });
|
|||
|
|
const engine = new THREE.Mesh(engineGeo, engineMat);
|
|||
|
|
engine.rotation.x = Math.PI / 2;
|
|||
|
|
engine.position.z = 1;
|
|||
|
|
this.group.add(engine);
|
|||
|
|
|
|||
|
|
// Shield sphere
|
|||
|
|
const shieldGeo = new THREE.SphereGeometry(1.5, 32, 32);
|
|||
|
|
const shieldMat = new THREE.MeshBasicMaterial({
|
|||
|
|
color: 0x00aaff,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0,
|
|||
|
|
side: THREE.DoubleSide
|
|||
|
|
});
|
|||
|
|
this.shieldMesh = new THREE.Mesh(shieldGeo, shieldMat);
|
|||
|
|
this.group.add(this.shieldMesh);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(input, deltaTime) {
|
|||
|
|
// Rotation
|
|||
|
|
if (input.mouseX !== 0 || input.mouseY !== 0) {
|
|||
|
|
const targetRotation = Math.atan2(input.mouseX, 1);
|
|||
|
|
this.group.rotation.z += (targetRotation - this.group.rotation.z) * this.turnSpeed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Movement
|
|||
|
|
this.acceleration.set(0, 0, 0);
|
|||
|
|
if (input.keys['w'] || input.keys['arrowup']) this.acceleration.y = 1;
|
|||
|
|
if (input.keys['s'] || input.keys['arrowdown']) this.acceleration.y = -1;
|
|||
|
|
if (input.keys['a'] || input.keys['arrowleft']) this.acceleration.x = -1;
|
|||
|
|
if (input.keys['d'] || input.keys['arrowright']) this.acceleration.x = 1;
|
|||
|
|
|
|||
|
|
if (this.acceleration.length() > 0) {
|
|||
|
|
this.acceleration.normalize().multiplyScalar(this.speed * this.engineLevel * 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.velocity.add(this.acceleration);
|
|||
|
|
this.velocity.multiplyScalar(0.95);
|
|||
|
|
this.group.position.add(this.velocity);
|
|||
|
|
|
|||
|
|
// Boundary wrap
|
|||
|
|
if (this.group.position.x > 200) this.group.position.x = -200;
|
|||
|
|
if (this.group.position.x < -200) this.group.position.x = 200;
|
|||
|
|
if (this.group.position.y > 150) this.group.position.y = -150;
|
|||
|
|
if (this.group.position.y < -150) this.group.position.y = 150;
|
|||
|
|
|
|||
|
|
// Shield
|
|||
|
|
if (input.keys[' '] && this.shield > 0) {
|
|||
|
|
this.shieldActive = true;
|
|||
|
|
this.shield -= deltaTime * 10;
|
|||
|
|
this.shieldMesh.material.opacity = 0.3;
|
|||
|
|
} else {
|
|||
|
|
this.shieldActive = false;
|
|||
|
|
this.shieldMesh.material.opacity = 0;
|
|||
|
|
if (this.shield < this.maxShield) {
|
|||
|
|
this.shieldRegenCooldown -= deltaTime;
|
|||
|
|
if (this.shieldRegenCooldown <= 0) {
|
|||
|
|
this.shield += deltaTime * 5 * this.shieldLevel;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
this.shield = Math.max(0, Math.min(this.maxShield, this.shield));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getMuzzlePosition() {
|
|||
|
|
const pos = new THREE.Vector3(0, 0, -1.2);
|
|||
|
|
pos.applyQuaternion(this.group.quaternion);
|
|||
|
|
pos.add(this.group.position);
|
|||
|
|
return pos;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
shoot() {
|
|||
|
|
const now = Date.now();
|
|||
|
|
if (now - this.lastShot < this.fireRate / (1 + this.weaponLevel * 0.2)) return null;
|
|||
|
|
this.lastShot = now;
|
|||
|
|
return {
|
|||
|
|
position: this.getMuzzlePosition(),
|
|||
|
|
direction: new THREE.Vector3(0, 0, -1).applyQuaternion(this.group.quaternion),
|
|||
|
|
damage: 10 * this.weaponLevel
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
takeDamage(amount) {
|
|||
|
|
if (this.shieldActive) {
|
|||
|
|
amount *= 0.3;
|
|||
|
|
}
|
|||
|
|
this.shield -= amount;
|
|||
|
|
this.shieldRegenCooldown = 3;
|
|||
|
|
return amount;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Planet ====================
|
|||
|
|
class Planet {
|
|||
|
|
constructor(scene, x, y) {
|
|||
|
|
this.scene = scene;
|
|||
|
|
this.group = new THREE.Group();
|
|||
|
|
this.position = new THREE.Vector2(x, y);
|
|||
|
|
this.scanned = false;
|
|||
|
|
this.hasResource = Math.random() < 0.3;
|
|||
|
|
this.resourceType = this.hasResource ?
|
|||
|
|
['mineral', 'crystal', 'artifact'][Math.floor(Math.random() * 3)] : null;
|
|||
|
|
|
|||
|
|
// Generate properties
|
|||
|
|
this.size = 3 + Math.random() * 8;
|
|||
|
|
this.hue = Math.random();
|
|||
|
|
this.hasRing = Math.random() < 0.2;
|
|||
|
|
this.hasAtmosphere = Math.random() < 0.6;
|
|||
|
|
|
|||
|
|
this.createModel();
|
|||
|
|
this.group.position.set(x, y, -20 - Math.random() * 30);
|
|||
|
|
this.scanData = this.generateScanData();
|
|||
|
|
scene.add(this.group);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createModel() {
|
|||
|
|
// Planet sphere
|
|||
|
|
const geometry = new THREE.SphereGeometry(this.size, 32, 32);
|
|||
|
|
const material = new THREE.MeshPhongMaterial({
|
|||
|
|
color: new THREE.Color().setHSL(this.hue, 0.6, 0.4),
|
|||
|
|
emissive: new THREE.Color().setHSL(this.hue, 0.4, 0.1)
|
|||
|
|
});
|
|||
|
|
this.planet = new THREE.Mesh(geometry, material);
|
|||
|
|
this.group.add(this.planet);
|
|||
|
|
|
|||
|
|
// Atmosphere
|
|||
|
|
if (this.hasAtmosphere) {
|
|||
|
|
const atmoGeo = new THREE.SphereGeometry(this.size * 1.1, 32, 32);
|
|||
|
|
const atmoMat = new THREE.MeshBasicMaterial({
|
|||
|
|
color: new THREE.Color().setHSL(this.hue, 0.8, 0.6),
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.2,
|
|||
|
|
side: THREE.BackSide
|
|||
|
|
});
|
|||
|
|
const atmosphere = new THREE.Mesh(atmoGeo, atmoMat);
|
|||
|
|
this.group.add(atmosphere);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ring
|
|||
|
|
if (this.hasRing) {
|
|||
|
|
const ringGeo = new THREE.RingGeometry(this.size * 1.4, this.size * 2, 64);
|
|||
|
|
const ringMat = new THREE.MeshBasicMaterial({
|
|||
|
|
color: new THREE.Color().setHSL(this.hue + 0.1, 0.5, 0.7),
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.6,
|
|||
|
|
side: THREE.DoubleSide
|
|||
|
|
});
|
|||
|
|
const ring = new THREE.Mesh(ringGeo, ringMat);
|
|||
|
|
ring.rotation.x = Math.PI / 2;
|
|||
|
|
ring.rotation.y = Math.random() * Math.PI;
|
|||
|
|
this.group.add(ring);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
generateScanData() {
|
|||
|
|
const types = ['气态巨行星', '岩石行星', '冰巨星', '熔岩世界', '海洋行星'];
|
|||
|
|
const habitability = ['不适宜居住', '可能存在微生物', '可能存在液态水', '可能存在生命', '高度适宜居住'];
|
|||
|
|
return {
|
|||
|
|
name: `PLANET-${Math.random().toString(36).substring(2, 8).toUpperCase()}`,
|
|||
|
|
type: types[Math.floor(Math.random() * types.length)],
|
|||
|
|
diameter: (this.size * 2000).toFixed(0) + ' km',
|
|||
|
|
temperature: Math.floor(-150 + Math.random() * 300) + '°C',
|
|||
|
|
gravity: (0.5 + Math.random() * 1.5).toFixed(2) + ' G',
|
|||
|
|
atmosphere: ['氮气', '氧气', '二氧化碳', '甲烷', '氢气'][Math.floor(Math.random() * 5)],
|
|||
|
|
habitability: habitability[Math.floor(Math.random() * habitability.length)],
|
|||
|
|
resources: this.hasResource ? {
|
|||
|
|
mineral: '稀有金属矿脉',
|
|||
|
|
crystal: '能量晶体矿床',
|
|||
|
|
artifact: '古代文明遗迹'
|
|||
|
|
}[this.resourceType] : '未检测到有价值资源'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(time) {
|
|||
|
|
this.planet.rotation.y += 0.001;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isNear(shipPosition, distance = 15) {
|
|||
|
|
const dx = this.group.position.x - shipPosition.x;
|
|||
|
|
const dy = this.group.position.y - shipPosition.y;
|
|||
|
|
return Math.sqrt(dx * dx + dy * dy) < distance;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Enemy ====================
|
|||
|
|
class Enemy {
|
|||
|
|
constructor(scene, shipPosition) {
|
|||
|
|
this.scene = scene;
|
|||
|
|
this.group = new THREE.Group();
|
|||
|
|
this.type = ['drone', 'pirate', 'asteroid'][Math.floor(Math.random() * 3)];
|
|||
|
|
this.health = this.type === 'asteroid' ? 30 : 20;
|
|||
|
|
this.maxHealth = this.health;
|
|||
|
|
this.damage = this.type === 'asteroid' ? 5 : 10;
|
|||
|
|
|
|||
|
|
// Spawn position (away from ship)
|
|||
|
|
const angle = Math.random() * Math.PI * 2;
|
|||
|
|
const distance = 30 + Math.random() * 50;
|
|||
|
|
this.group.position.set(
|
|||
|
|
shipPosition.x + Math.cos(angle) * distance,
|
|||
|
|
shipPosition.y + Math.sin(angle) * distance,
|
|||
|
|
-25
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
this.createModel();
|
|||
|
|
scene.add(this.group);
|
|||
|
|
this.lastAttack = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createModel() {
|
|||
|
|
if (this.type === 'drone') {
|
|||
|
|
const body = new THREE.Mesh(
|
|||
|
|
new THREE.OctahedronGeometry(0.8),
|
|||
|
|
new THREE.MeshPhongMaterial({ color: 0xff3333, emissive: 0x330000 })
|
|||
|
|
);
|
|||
|
|
this.group.add(body);
|
|||
|
|
this.speed = 0.08;
|
|||
|
|
} else if (this.type === 'pirate') {
|
|||
|
|
const body = new THREE.Mesh(
|
|||
|
|
new THREE.BoxGeometry(1.5, 0.8, 2),
|
|||
|
|
new THREE.MeshPhongMaterial({ color: 0xff6600, emissive: 0x331100 })
|
|||
|
|
);
|
|||
|
|
this.group.add(body);
|
|||
|
|
this.speed = 0.05;
|
|||
|
|
} else {
|
|||
|
|
const asteroid = new THREE.Mesh(
|
|||
|
|
new THREE.DodecahedronGeometry(1.5),
|
|||
|
|
new THREE.MeshPhongMaterial({ color: 0x888888 })
|
|||
|
|
);
|
|||
|
|
this.group.add(asteroid);
|
|||
|
|
this.speed = 0.02;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(shipPosition, deltaTime) {
|
|||
|
|
// Move towards ship
|
|||
|
|
const dx = shipPosition.x - this.group.position.x;
|
|||
|
|
const dy = shipPosition.y - this.group.position.y;
|
|||
|
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|||
|
|
|
|||
|
|
if (dist > 5) {
|
|||
|
|
this.group.position.x += (dx / dist) * this.speed;
|
|||
|
|
this.group.position.y += (dy / dist) * this.speed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Rotation
|
|||
|
|
this.group.rotation.x += deltaTime;
|
|||
|
|
this.group.rotation.z += deltaTime * 0.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
takeDamage(amount) {
|
|||
|
|
this.health -= amount;
|
|||
|
|
// Flash effect
|
|||
|
|
this.group.children.forEach(child => {
|
|||
|
|
if (child.material) {
|
|||
|
|
child.material.emissive = new THREE.Color(0xffffff);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (child.material) {
|
|||
|
|
child.material.emissive = new THREE.Color(
|
|||
|
|
this.type === 'asteroid' ? 0x000000 :
|
|||
|
|
this.type === 'drone' ? 0x330000 : 0x331100
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}, 50);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return this.health <= 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Starfield ====================
|
|||
|
|
class Starfield {
|
|||
|
|
constructor(scene) {
|
|||
|
|
this.scene = scene;
|
|||
|
|
this.createStars();
|
|||
|
|
this.createNebula();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createStars() {
|
|||
|
|
const starGeo = new THREE.BufferGeometry();
|
|||
|
|
const starCount = 3000;
|
|||
|
|
const positions = new Float32Array(starCount * 3);
|
|||
|
|
const colors = new Float32Array(starCount * 3);
|
|||
|
|
const sizes = new Float32Array(starCount);
|
|||
|
|
|
|||
|
|
for (let i = 0; i < starCount; i++) {
|
|||
|
|
positions[i * 3] = (Math.random() - 0.5) * 800;
|
|||
|
|
positions[i * 3 + 1] = (Math.random() - 0.5) * 600;
|
|||
|
|
positions[i * 3 + 2] = -100 - Math.random() * 200;
|
|||
|
|
|
|||
|
|
const color = new THREE.Color();
|
|||
|
|
color.setHSL(Math.random() * 0.2 + 0.5, 0.8, 0.8);
|
|||
|
|
colors[i * 3] = color.r;
|
|||
|
|
colors[i * 3 + 1] = color.g;
|
|||
|
|
colors[i * 3 + 2] = color.b;
|
|||
|
|
|
|||
|
|
sizes[i] = Math.random() * 2 + 0.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|||
|
|
starGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
|||
|
|
starGeo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
|||
|
|
|
|||
|
|
const starMat = new THREE.PointsMaterial({
|
|||
|
|
size: 1.5,
|
|||
|
|
vertexColors: true,
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.8,
|
|||
|
|
sizeAttenuation: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.stars = new THREE.Points(starGeo, starMat);
|
|||
|
|
this.scene.add(this.stars);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createNebula() {
|
|||
|
|
const nebulaGeo = new THREE.PlaneGeometry(600, 400);
|
|||
|
|
const nebulaMat = new THREE.MeshBasicMaterial({
|
|||
|
|
transparent: true,
|
|||
|
|
opacity: 0.15,
|
|||
|
|
side: THREE.DoubleSide,
|
|||
|
|
blending: THREE.AdditiveBlending
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.nebula = new THREE.Mesh(nebulaGeo, nebulaMat);
|
|||
|
|
this.nebula.position.z = -150;
|
|||
|
|
this.scene.add(this.nebula);
|
|||
|
|
|
|||
|
|
// Create gradient texture for nebula
|
|||
|
|
const canvas = document.createElement('canvas');
|
|||
|
|
canvas.width = 256;
|
|||
|
|
canvas.height = 256;
|
|||
|
|
const ctx = canvas.getContext('2d');
|
|||
|
|
const gradient = ctx.createRadialGradient(128, 128, 0, 128, 128, 128);
|
|||
|
|
gradient.addColorStop(0, 'rgba(100, 50, 150, 0.8)');
|
|||
|
|
gradient.addColorStop(0.3, 'rgba(50, 100, 200, 0.4)');
|
|||
|
|
gradient.addColorStop(0.6, 'rgba(30, 150, 200, 0.2)');
|
|||
|
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|||
|
|
ctx.fillStyle = gradient;
|
|||
|
|
ctx.fillRect(0, 0, 256, 256);
|
|||
|
|
|
|||
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|||
|
|
nebulaMat.map = texture;
|
|||
|
|
nebulaMat.needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(shipPosition) {
|
|||
|
|
// Parallax effect
|
|||
|
|
this.stars.position.x = shipPosition.x * 0.1;
|
|||
|
|
this.stars.position.y = shipPosition.y * 0.1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Input Handler ====================
|
|||
|
|
class InputHandler {
|
|||
|
|
constructor() {
|
|||
|
|
this.keys = {};
|
|||
|
|
this.mouseX = 0;
|
|||
|
|
this.mouseY = 0;
|
|||
|
|
this.mouseDown = false;
|
|||
|
|
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
this.keys[e.key.toLowerCase()] = true;
|
|||
|
|
});
|
|||
|
|
document.addEventListener('keyup', (e) => {
|
|||
|
|
this.keys[e.key.toLowerCase()] = false;
|
|||
|
|
});
|
|||
|
|
document.addEventListener('mousemove', (e) => {
|
|||
|
|
this.mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
|
|||
|
|
this.mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
|
|||
|
|
});
|
|||
|
|
document.addEventListener('mousedown', () => {
|
|||
|
|
this.mouseDown = true;
|
|||
|
|
});
|
|||
|
|
document.addEventListener('mouseup', () => {
|
|||
|
|
this.mouseDown = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== Game Class ====================
|
|||
|
|
class Game {
|
|||
|
|
constructor() {
|
|||
|
|
this.canvas = document.getElementById('game-canvas');
|
|||
|
|
this.scene = new THREE.Scene();
|
|||
|
|
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|||
|
|
this.camera.position.z = 50;
|
|||
|
|
|
|||
|
|
this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas, antialias: true });
|
|||
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|||
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|||
|
|
|
|||
|
|
// Lighting
|
|||
|
|
const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
|
|||
|
|
this.scene.add(ambientLight);
|
|||
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|||
|
|
directionalLight.position.set(5, 5, 10);
|
|||
|
|
this.scene.add(directionalLight);
|
|||
|
|
|
|||
|
|
// Systems
|
|||
|
|
this.input = new InputHandler();
|
|||
|
|
this.audio = new AudioSystem();
|
|||
|
|
this.particles = new ParticleSystem(this.scene);
|
|||
|
|
this.starfield = new Starfield(this.scene);
|
|||
|
|
|
|||
|
|
// Game state
|
|||
|
|
this.spaceship = null;
|
|||
|
|
this.planets = [];
|
|||
|
|
this.enemies = [];
|
|||
|
|
this.projectiles = [];
|
|||
|
|
this.resources = { mineral: 0, crystal: 0, artifact: 0 };
|
|||
|
|
this.score = 0;
|
|||
|
|
this.level = 1;
|
|||
|
|
this.xp = 0;
|
|||
|
|
this.xpToLevel = 100;
|
|||
|
|
this.isPaused = true;
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
this.inCombat = false;
|
|||
|
|
|
|||
|
|
// Last times
|
|||
|
|
this.lastTime = 0;
|
|||
|
|
this.planetSpawnTimer = 0;
|
|||
|
|
this.enemySpawnTimer = 0;
|
|||
|
|
|
|||
|
|
// Minimap
|
|||
|
|
this.setupMinimap();
|
|||
|
|
|
|||
|
|
// Window resize
|
|||
|
|
window.addEventListener('resize', () => this.onResize());
|
|||
|
|
|
|||
|
|
console.log('游戏引擎初始化完成');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupMinimap() {
|
|||
|
|
this.minimapCanvas = document.getElementById('minimap-canvas');
|
|||
|
|
this.minimapCtx = this.minimapCanvas.getContext('2d');
|
|||
|
|
this.minimapCanvas.width = 150;
|
|||
|
|
this.minimapCanvas.height = 150;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateMinimap() {
|
|||
|
|
const ctx = this.minimapCtx;
|
|||
|
|
ctx.fillStyle = 'rgba(0, 10, 30, 0.8)';
|
|||
|
|
ctx.fillRect(0, 0, 150, 150);
|
|||
|
|
|
|||
|
|
const scale = 0.3;
|
|||
|
|
const centerX = 75;
|
|||
|
|
const centerY = 75;
|
|||
|
|
|
|||
|
|
// Draw planets
|
|||
|
|
ctx.fillStyle = '#4466aa';
|
|||
|
|
this.planets.forEach(planet => {
|
|||
|
|
const x = centerX + (planet.position.x - this.spaceship.group.position.x) * scale;
|
|||
|
|
const y = centerY + (planet.position.y - this.spaceship.group.position.y) * scale;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
|||
|
|
ctx.fill();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Draw enemies
|
|||
|
|
ctx.fillStyle = '#ff4444';
|
|||
|
|
this.enemies.forEach(enemy => {
|
|||
|
|
const x = centerX + (enemy.group.position.x - this.spaceship.group.position.x) * scale;
|
|||
|
|
const y = centerY + (enemy.group.position.y - this.spaceship.group.position.y) * scale;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(x, y, 2, 0, Math.PI * 2);
|
|||
|
|
ctx.fill();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Draw ship
|
|||
|
|
ctx.fillStyle = '#00ffff';
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(centerX, centerY, 3, 0, Math.PI * 2);
|
|||
|
|
ctx.fill();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
start() {
|
|||
|
|
document.getElementById('start-screen').classList.add('hidden');
|
|||
|
|
document.getElementById('loading-screen').classList.add('hidden');
|
|||
|
|
|
|||
|
|
// Show HUD
|
|||
|
|
document.getElementById('hud-top').style.display = 'flex';
|
|||
|
|
document.getElementById('shield-bar').style.display = 'block';
|
|||
|
|
document.getElementById('xp-bar').style.display = 'block';
|
|||
|
|
document.getElementById('resource-panel').style.display = 'block';
|
|||
|
|
document.getElementById('minimap').style.display = 'block';
|
|||
|
|
document.getElementById('crosshair').style.display = 'block';
|
|||
|
|
|
|||
|
|
// Initialize systems
|
|||
|
|
this.audio.init();
|
|||
|
|
this.spaceship = new Spaceship(this.scene);
|
|||
|
|
|
|||
|
|
// Spawn initial planets
|
|||
|
|
for (let i = 0; i < 50; i++) {
|
|||
|
|
this.spawnPlanet();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.isPaused = false;
|
|||
|
|
this.isPlaying = true;
|
|||
|
|
this.lastTime = Date.now();
|
|||
|
|
|
|||
|
|
// Start game loop
|
|||
|
|
this.gameLoop();
|
|||
|
|
|
|||
|
|
this.showNotification('探索者,欢迎来到星际文明!按 E 扫描附近星球');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
spawnPlanet() {
|
|||
|
|
const x = (Math.random() - 0.5) * 400;
|
|||
|
|
const y = (Math.random() - 0.5) * 300;
|
|||
|
|
this.planets.push(new Planet(this.scene, x, y));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
spawnEnemy() {
|
|||
|
|
this.enemies.push(new Enemy(this.scene, this.spaceship.group.position));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameLoop() {
|
|||
|
|
if (!this.isPaused) {
|
|||
|
|
requestAnimationFrame(() => this.gameLoop());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const now = Date.now();
|
|||
|
|
const deltaTime = (now - this.lastTime) / 1000;
|
|||
|
|
this.lastTime = now;
|
|||
|
|
|
|||
|
|
this.update(deltaTime);
|
|||
|
|
this.render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update(deltaTime) {
|
|||
|
|
// Input
|
|||
|
|
this.input.keys;
|
|||
|
|
|
|||
|
|
const now = Date.now();
|
|||
|
|
|
|||
|
|
// Update spaceship
|
|||
|
|
if (this.spaceship) {
|
|||
|
|
this.spaceship.update(this.input, deltaTime);
|
|||
|
|
|
|||
|
|
// Shooting
|
|||
|
|
if (this.input.mouseDown) {
|
|||
|
|
const shot = this.spaceship.shoot();
|
|||
|
|
if (shot) {
|
|||
|
|
this.projectiles.push({
|
|||
|
|
position: shot.position.clone(),
|
|||
|
|
direction: shot.direction,
|
|||
|
|
damage: shot.damage,
|
|||
|
|
life: 2
|
|||
|
|
});
|
|||
|
|
this.audio.playShoot();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update starfield
|
|||
|
|
this.starfield.update(this.spaceship.group.position);
|
|||
|
|
|
|||
|
|
// Update planets
|
|||
|
|
this.planets.forEach(planet => planet.update(now * 0.001));
|
|||
|
|
|
|||
|
|
// Update enemies
|
|||
|
|
this.enemies.forEach(enemy => enemy.update(this.spaceship.group.position, deltaTime));
|
|||
|
|
|
|||
|
|
// Spawn new planets
|
|||
|
|
this.planetSpawnTimer += deltaTime;
|
|||
|
|
if (this.planetSpawnTimer > 5 && this.planets.length < 80) {
|
|||
|
|
this.spawnPlanet();
|
|||
|
|
this.planetSpawnTimer = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Spawn enemies
|
|||
|
|
this.enemySpawnTimer += deltaTime;
|
|||
|
|
if (this.enemySpawnTimer > 8 && this.enemies.length < 15) {
|
|||
|
|
this.spawnEnemy();
|
|||
|
|
this.enemySpawnTimer = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update projectiles
|
|||
|
|
for (let i = this.projectiles.length - 1; i >= 0; i--) {
|
|||
|
|
const p = this.projectiles[i];
|
|||
|
|
p.position.add(p.direction.clone().multiplyScalar(2));
|
|||
|
|
p.life -= deltaTime;
|
|||
|
|
|
|||
|
|
// Draw laser
|
|||
|
|
this.particles.createLaser(
|
|||
|
|
p.position.clone().add(p.direction.clone().multiplyScalar(-0.5)),
|
|||
|
|
p.position,
|
|||
|
|
0x00ffff
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// Check enemy collisions
|
|||
|
|
for (let j = this.enemies.length - 1; j >= 0; j--) {
|
|||
|
|
const enemy = this.enemies[j];
|
|||
|
|
const dist = p.position.distanceTo(enemy.group.position);
|
|||
|
|
if (dist < 2) {
|
|||
|
|
const killed = enemy.takeDamage(p.damage);
|
|||
|
|
if (killed) {
|
|||
|
|
this.enemies.splice(j, 1);
|
|||
|
|
this.particles.createExplosion(enemy.group.position.clone());
|
|||
|
|
this.audio.playExplosion();
|
|||
|
|
this.score += enemy.type === 'pirate' ? 50 : 30;
|
|||
|
|
this.addXP(enemy.type === 'pirate' ? 25 : 15);
|
|||
|
|
}
|
|||
|
|
this.projectiles.splice(i, 1);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (p.life <= 0) {
|
|||
|
|
this.projectiles.splice(i, 1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check enemy-ship collisions
|
|||
|
|
this.inCombat = false;
|
|||
|
|
for (let i = this.enemies.length - 1; i >= 0; i--) {
|
|||
|
|
const enemy = this.enemies[i];
|
|||
|
|
const dist = enemy.group.position.distanceTo(this.spaceship.group.position);
|
|||
|
|
if (dist < 3) {
|
|||
|
|
this.inCombat = true;
|
|||
|
|
this.spaceship.takeDamage(enemy.damage * deltaTime);
|
|||
|
|
this.particles.createExplosion(
|
|||
|
|
this.spaceship.group.position.clone(),
|
|||
|
|
0x00aaff
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update particles
|
|||
|
|
this.particles.update();
|
|||
|
|
|
|||
|
|
// Scan planets
|
|||
|
|
if (this.input.keys['e']) {
|
|||
|
|
this.scanNearbyPlanets();
|
|||
|
|
this.input.keys['e'] = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Update UI
|
|||
|
|
this.updateUI();
|
|||
|
|
this.updateMinimap();
|
|||
|
|
|
|||
|
|
// Check game over
|
|||
|
|
if (this.spaceship.shield <= 0) {
|
|||
|
|
this.gameOver();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
scanNearbyPlanets() {
|
|||
|
|
let scanned = false;
|
|||
|
|
this.planets.forEach(planet => {
|
|||
|
|
if (planet.isNear(this.spaceship.group.position) && !planet.scanned) {
|
|||
|
|
planet.scanned = true;
|
|||
|
|
scanned = true;
|
|||
|
|
this.showScanPanel(planet);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
if (!scanned) {
|
|||
|
|
this.showNotification('附近未发现可扫描的星球');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showScanPanel(planet) {
|
|||
|
|
const panel = document.getElementById('scan-panel');
|
|||
|
|
const data = planet.scanData;
|
|||
|
|
document.getElementById('scan-name').textContent = data.name;
|
|||
|
|
document.getElementById('scan-data').innerHTML = `
|
|||
|
|
类型: ${data.type}<br>
|
|||
|
|
直径: ${data.diameter}<br>
|
|||
|
|
温度: ${data.temperature}<br>
|
|||
|
|
重力: ${data.gravity}<br>
|
|||
|
|
大气: ${data.atmosphere}<br>
|
|||
|
|
宜居度: ${data.habitability}<br>
|
|||
|
|
资源: ${data.resources}
|
|||
|
|
`;
|
|||
|
|
panel.classList.add('active');
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
panel.classList.remove('active');
|
|||
|
|
}, 5000);
|
|||
|
|
|
|||
|
|
// Collect resource if available
|
|||
|
|
if (planet.hasResource) {
|
|||
|
|
this.resources[planet.resourceType]++;
|
|||
|
|
this.audio.playCollect();
|
|||
|
|
this.showNotification(`发现 ${planet.resourceType === 'mineral' ? '矿物' : planet.resourceType === 'crystal' ? '能量晶体' : '遗迹碎片'}!`);
|
|||
|
|
this.score += 100;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
addXP(amount) {
|
|||
|
|
this.xp += amount;
|
|||
|
|
if (this.xp >= this.xpToLevel) {
|
|||
|
|
this.levelUp();
|
|||
|
|
}
|
|||
|
|
this.updateUI();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
levelUp() {
|
|||
|
|
this.level++;
|
|||
|
|
this.xp -= this.xpToLevel;
|
|||
|
|
this.xpToLevel = Math.floor(this.xpToLevel * 1.5);
|
|||
|
|
this.spaceship.maxShield = 100 + (this.level - 1) * 20;
|
|||
|
|
this.spaceship.shield = this.spaceship.maxShield;
|
|||
|
|
|
|||
|
|
// Show level up effect
|
|||
|
|
const overlay = document.getElementById('levelup-overlay');
|
|||
|
|
overlay.classList.add('active');
|
|||
|
|
this.audio.playLevelUp();
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
overlay.classList.remove('active');
|
|||
|
|
}, 1000);
|
|||
|
|
|
|||
|
|
// Show upgrade panel
|
|||
|
|
this.showUpgradePanel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showUpgradePanel() {
|
|||
|
|
document.getElementById('upgrade-panel').classList.add('active');
|
|||
|
|
this.updateUpgradeUI();
|
|||
|
|
this.isPaused = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateUpgradeUI() {
|
|||
|
|
document.getElementById('engine-level').textContent = `当前等级: ${this.spaceship.engineLevel}`;
|
|||
|
|
document.getElementById('shield-level').textContent = `当前等级: ${this.spaceship.shieldLevel}`;
|
|||
|
|
document.getElementById('weapon-level').textContent = `当前等级: ${this.spaceship.weaponLevel}`;
|
|||
|
|
document.getElementById('scanner-level').textContent = `当前等级: ${this.spaceship.scannerLevel}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
selectUpgrade(type) {
|
|||
|
|
const cost = (this.resources.mineral + this.resources.crystal + this.resources.artifact) >= 10;
|
|||
|
|
|
|||
|
|
if (type === 'engine') this.spaceship.engineLevel++;
|
|||
|
|
if (type === 'shield') this.spaceship.shieldLevel++;
|
|||
|
|
if (type === 'weapon') this.spaceship.weaponLevel++;
|
|||
|
|
if (type === 'scanner') this.spaceship.scannerLevel++;
|
|||
|
|
|
|||
|
|
this.showNotification(`${type === 'engine' ? '推进系统' : type === 'shield' ? '能量护盾' : type === 'weapon' ? '武器系统' : '扫描仪'}已升级!`);
|
|||
|
|
document.getElementById('upgrade-panel').classList.remove('active');
|
|||
|
|
this.isPaused = false;
|
|||
|
|
this.lastTime = Date.now();
|
|||
|
|
this.gameLoop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateUI() {
|
|||
|
|
document.getElementById('level-display').textContent = this.level;
|
|||
|
|
document.getElementById('score-display').textContent = this.score;
|
|||
|
|
document.getElementById('enemy-display').textContent = this.enemies.length;
|
|||
|
|
|
|||
|
|
document.getElementById('shield-fill').style.width = `${this.spaceship.shield}%`;
|
|||
|
|
document.getElementById('shield-text').textContent = `护盾 ${Math.floor(this.spaceship.shield)}%`;
|
|||
|
|
|
|||
|
|
document.getElementById('xp-fill').style.width = `${(this.xp / this.xpToLevel) * 100}%`;
|
|||
|
|
document.getElementById('xp-text').textContent = `${this.xp} / ${this.xpToLevel} XP`;
|
|||
|
|
|
|||
|
|
document.getElementById('mineral-count').textContent = this.resources.mineral;
|
|||
|
|
document.getElementById('crystal-count').textContent = this.resources.crystal;
|
|||
|
|
document.getElementById('artifact-count').textContent = this.resources.artifact;
|
|||
|
|
|
|||
|
|
const combatIndicator = document.getElementById('combat-indicator');
|
|||
|
|
if (this.inCombat) {
|
|||
|
|
combatIndicator.classList.add('active');
|
|||
|
|
} else {
|
|||
|
|
combatIndicator.classList.remove('active');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
render() {
|
|||
|
|
this.renderer.render(this.scene, this.camera);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showNotification(message) {
|
|||
|
|
const notif = document.getElementById('notification');
|
|||
|
|
notif.textContent = message;
|
|||
|
|
notif.classList.add('show');
|
|||
|
|
setTimeout(() => notif.classList.remove('show'), 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pause() {
|
|||
|
|
this.isPaused = true;
|
|||
|
|
document.getElementById('pause-menu').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resume() {
|
|||
|
|
this.isPaused = false;
|
|||
|
|
document.getElementById('pause-menu').classList.remove('active');
|
|||
|
|
document.getElementById('help-panel').classList.remove('active');
|
|||
|
|
this.lastTime = Date.now();
|
|||
|
|
this.gameLoop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showHelp() {
|
|||
|
|
document.getElementById('pause-menu').classList.remove('active');
|
|||
|
|
document.getElementById('help-panel').classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
hideHelp() {
|
|||
|
|
document.getElementById('help-panel').classList.remove('active');
|
|||
|
|
this.resume();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saveGame() {
|
|||
|
|
const saveData = {
|
|||
|
|
level: this.level,
|
|||
|
|
xp: this.xp,
|
|||
|
|
xpToLevel: this.xpToLevel,
|
|||
|
|
score: this.score,
|
|||
|
|
resources: this.resources,
|
|||
|
|
engineLevel: this.spaceship.engineLevel,
|
|||
|
|
shieldLevel: this.spaceship.shieldLevel,
|
|||
|
|
weaponLevel: this.spaceship.weaponLevel,
|
|||
|
|
scannerLevel: this.spaceship.scannerLevel
|
|||
|
|
};
|
|||
|
|
localStorage.setItem('stellarExplorer', JSON.stringify(saveData));
|
|||
|
|
this.showNotification('游戏已保存');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadGame() {
|
|||
|
|
const saved = localStorage.getItem('stellarExplorer');
|
|||
|
|
if (saved) {
|
|||
|
|
const data = JSON.parse(saved);
|
|||
|
|
this.level = data.level || 1;
|
|||
|
|
this.xp = data.xp || 0;
|
|||
|
|
this.xpToLevel = data.xpToLevel || 100;
|
|||
|
|
this.score = data.score || 0;
|
|||
|
|
this.resources = data.resources || { mineral: 0, crystal: 0, artifact: 0 };
|
|||
|
|
if (this.spaceship) {
|
|||
|
|
this.spaceship.engineLevel = data.engineLevel || 1;
|
|||
|
|
this.spaceship.shieldLevel = data.shieldLevel || 1;
|
|||
|
|
this.spaceship.weaponLevel = data.weaponLevel || 1;
|
|||
|
|
this.spaceship.scannerLevel = data.scannerLevel || 1;
|
|||
|
|
this.spaceship.maxShield = 100 + (this.level - 1) * 20;
|
|||
|
|
this.spaceship.shield = this.spaceship.maxShield;
|
|||
|
|
}
|
|||
|
|
this.showNotification('存档已读取');
|
|||
|
|
} else {
|
|||
|
|
this.showNotification('未找到存档');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
returnToMenu() {
|
|||
|
|
location.reload();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
gameOver() {
|
|||
|
|
this.isPlaying = false;
|
|||
|
|
this.showNotification(`游戏结束!最终得分: ${this.score}`);
|
|||
|
|
setTimeout(() => {
|
|||
|
|
this.returnToMenu();
|
|||
|
|
}, 3000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onResize() {
|
|||
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|||
|
|
this.camera.updateProjectionMatrix();
|
|||
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Initialize game
|
|||
|
|
const game = new Game();
|
|||
|
|
|
|||
|
|
// Global keyboard handler for pause
|
|||
|
|
document.addEventListener('keydown', (e) => {
|
|||
|
|
if (e.key === 'Escape' && game.isPlaying) {
|
|||
|
|
if (game.isPaused) {
|
|||
|
|
game.resume();
|
|||
|
|
} else {
|
|||
|
|
game.pause();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Hide loading screen after initialization
|
|||
|
|
window.addEventListener('load', () => {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
document.getElementById('loading-screen').classList.add('hidden');
|
|||
|
|
}, 1000);
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|