模块化空间站 · 完整度系统
by ApexFalcon661104 lines33.8 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>模块化空间站 · 完整度系统</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; }
body {
background: radial-gradient(circle at 50% 45%, #0c1228 0%, #03050c 100%);
font-family: system-ui, -apple-system, sans-serif;
-webkit-user-select:none; user-select:none;
}
/* ===== 视角容器 ===== */
.viewport {
position: fixed; inset: 0;
overflow: hidden;
cursor: grab;
touch-action: none;
}
.viewport.grabbing { cursor: grabbing; }
.scene {
position: absolute;
left: 50%; top: 50%;
width: 1200px; height: 1200px;
margin-left: -600px; margin-top: -600px;
transform-origin: 50% 50%;
will-change: transform;
}
/* ===== 星空 ===== */
.star-layer { position:absolute; inset:0; }
.star { position:absolute; background:#fff; border-radius:50%; animation: twinkle 3s infinite ease-in-out; }
@keyframes twinkle { 0%,100% { opacity:.15; } 50% { opacity:.95; } }
/* ===== 空间站 ===== */
.station {
position: absolute;
left: 380px; top: 380px;
width: 440px; height: 440px;
animation: float 7s ease-in-out infinite;
}
@keyframes float {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.mod {
position:absolute;
background:#fff;
border-radius:3px;
box-shadow: 0 3px 14px rgba(0,0,0,.45), inset 0 -2px 5px rgba(150,170,200,.25);
}
.core { width:96px; height:24px; left:172px; top:208px; }
.arm-left { width:80px; height:24px; left:92px; top:208px; }
.arm-right { width:80px; height:24px; left:268px; top:208px; }
.arm-up { width:24px; height:80px; left:208px; top:128px; }
.arm-down { width:24px; height:80px; left:208px; top:232px; }
.panel {
position:absolute;
width:26px; height:72px;
background: repeating-linear-gradient(0deg, #1d4d86 0px, #1d4d86 8px, #2b6cb5 8px, #2b6cb5 10px);
border:2px solid #4a90d9;
border-radius:2px;
box-shadow: 0 0 16px rgba(74,144,217,.35);
}
.panel-lt { left:79px; top:136px; }
.panel-lb { left:79px; top:232px; }
.panel-rt { left:335px; top:136px; }
.panel-rb { left:335px; top:232px; }
.win {
position:absolute;
width:8px; height:4px;
background:#2d4d6e;
border-radius:1px;
box-shadow: 0 0 4px rgba(120,190,255,.6);
}
.port { position:absolute; background:#b9c6d4; border:1px solid #8496aa; border-radius:2px; }
.port-v { width:10px; height:12px; }
.ship {
position:absolute;
width:30px; height:14px;
background:#e8eef5;
border:2px solid #98aabd;
border-radius:3px;
}
/* ===== 底部推进器 ===== */
.thruster { position:absolute; left:206px; top:324px; width:28px; height:14px; }
.thruster .bell {
width:100%; height:100%;
background: linear-gradient(180deg,#eef3f9 0%,#b7c4d3 55%,#8a9bb0 100%);
clip-path: polygon(0 0, 100% 0, 82% 100%, 18% 100%);
}
.flame { position:absolute; left:50%; top:13px; width:60px; height:80px; transform:translateX(-50%); pointer-events:none; }
.flame-outer {
position:absolute; left:50%; top:0;
width:30px; height:62px;
transform:translateX(-50%); transform-origin:50% 0;
background: radial-gradient(ellipse at 50% 12%, #ffd166 0%, #ff9a1f 35%, #ff5f00 68%, rgba(255,90,0,0) 100%);
clip-path: polygon(50% 100%, 0 0, 100% 0);
filter: blur(.6px);
animation: flameFlicker .16s infinite alternate ease-in-out;
}
.flame-core {
position:absolute; left:50%; top:0;
width:13px; height:34px;
transform:translateX(-50%); transform-origin:50% 0;
background: radial-gradient(ellipse at 50% 15%, #ffffff 0%, #fff4b8 45%, #ffc247 100%);
clip-path: polygon(50% 100%, 0 0, 100% 0);
animation: flameFlicker .13s infinite alternate-reverse ease-in-out;
}
.flame-glow {
position:absolute; left:50%; top:-6px;
width:80px; height:80px;
transform:translateX(-50%);
background: radial-gradient(circle, rgba(255,150,40,.42) 0%, rgba(255,120,0,0) 68%);
border-radius:50%;
animation: glowPulse .5s infinite alternate ease-in-out;
}
@keyframes flameFlicker {
from { transform: translateX(-50%) scale(.88,.78); opacity:.82; }
to { transform: translateX(-50%) scale(1.06,1.15); opacity:1; }
}
@keyframes glowPulse {
from { opacity:.55; transform: translateX(-50%) scale(.9); }
to { opacity:.95; transform: translateX(-50%) scale(1.1); }
}
/* ===== 上方电磁炮 ===== */
.rail-turret { position:absolute; left:220px; top:122px; width:0; height:0; z-index:12; }
.rail-base {
position:absolute; width:30px; height:30px;
left:-15px; top:-15px;
border-radius:5px;
background: linear-gradient(180deg,#ffffff,#b9c6d4);
border:2px solid #8a9bb0;
box-shadow: 0 0 14px rgba(120,200,255,.6);
}
.rail-barrel {
position:absolute; left:0; top:-5px;
width:54px; height:10px;
transform-origin:0 50%;
background: linear-gradient(180deg,#f4f9ff 0%,#b8c6d6 55%,#7f8ea3 100%);
border:1px solid #6a7e92;
border-radius:0 3px 3px 0;
box-shadow: 0 0 8px rgba(120,200,255,.4);
transition: transform .09s linear;
}
/* ===== 核心舱激光炮 ===== */
.laser-turret { position:absolute; left:220px; top:220px; width:0; height:0; z-index:13; }
.laser-base {
position:absolute; width:24px; height:24px;
left:-12px; top:-12px;
border-radius:50%;
background: radial-gradient(circle at 35% 30%, #ffffff, #9fc4e8 70%, #6a8aaa);
border:2px solid #5f7fa0;
box-shadow: 0 0 14px rgba(90,190,255,.7);
}
.laser-barrel {
position:absolute; left:0; top:-3px;
width:30px; height:6px;
transform-origin:0 50%;
background: linear-gradient(90deg,#e8f8ff 0%,#6fc8ff 60%,#3aa6e8 100%);
border:1px solid #2f9ae0;
border-radius:0 3px 3px 0;
box-shadow: 0 0 10px rgba(80,190,255,.85);
transition: transform .09s linear;
}
/* ===== 左右导弹巢 ===== */
.missile-pod {
position:absolute;
width:18px; height:28px;
top:206px;
border-radius:4px;
background:
radial-gradient(circle at 30% 22%, #2d4d6e 2px, transparent 2.4px),
radial-gradient(circle at 70% 22%, #2d4d6e 2px, transparent 2.4px),
radial-gradient(circle at 30% 50%, #2d4d6e 2px, transparent 2.4px),
radial-gradient(circle at 70% 50%, #2d4d6e 2px, transparent 2.4px),
radial-gradient(circle at 30% 78%, #2d4d6e 2px, transparent 2.4px),
radial-gradient(circle at 70% 78%, #2d4d6e 2px, transparent 2.4px),
linear-gradient(180deg,#ffffff,#b9c6d4);
border:2px solid #8a9bb0;
box-shadow: 0 0 10px rgba(255,190,120,.35);
z-index:12;
}
.missile-pod.left { left:81px; }
.missile-pod.right { left:341px; }
/* ===== 模块完整度血条 ===== */
.hp-bar {
position: absolute;
height: 4px;
background: rgba(0,0,0,.55);
border: 1px solid rgba(160,200,255,.35);
border-radius: 2px;
opacity: 0;
transition: opacity .4s;
pointer-events: none;
z-index: 30;
box-shadow: 0 0 6px rgba(0,0,0,.6);
overflow: hidden;
}
.hp-fill {
height: 100%;
width: 100%;
background: #4ade80;
transition: width .18s linear, background .3s, box-shadow .3s;
box-shadow: 0 0 6px rgba(74,222,128,.7);
}
/* ===== 碎片 ===== */
.fragment {
position: absolute;
border-radius: 1px;
pointer-events: none;
z-index: 26;
animation: fragFly .9s ease-out forwards;
}
@keyframes fragFly {
0% { transform: translate(0,0) rotate(0deg); opacity: 1; }
100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}
/* ===== 陨石 ===== */
.meteor {
position: absolute;
left: 0; top: 0;
border-radius: 50%;
background: radial-gradient(circle at 34% 30%, #cfc0b2 0%, #8a7a6d 45%, #4a3f38 78%, #2b241f 100%);
box-shadow: 0 0 14px rgba(255,160,80,.32), inset -3px -3px 8px rgba(0,0,0,.65);
pointer-events: none;
z-index: 8;
will-change: transform;
}
.meteor::after {
content:"";
position:absolute;
inset:-7px;
border-radius:50%;
background: radial-gradient(circle, rgba(255,170,90,.3) 0%, rgba(255,140,60,0) 70%);
pointer-events:none;
}
/* ===== 弹体 ===== */
.projectile { position:absolute; left:0; top:0; pointer-events:none; }
.rail-slug {
width:34px; height:6px;
margin:-3px 0 0 -17px;
border-radius:3px;
background: linear-gradient(90deg, rgba(120,220,255,0) 0%, #7fdcff 45%, #ffffff 80%, #ffffff 100%);
box-shadow: 0 0 12px #7fdcff, 0 0 26px rgba(127,220,255,.6);
}
.missile {
width:16px; height:6px;
margin:-3px 0 0 -8px;
border-radius:0 3px 3px 0;
background: linear-gradient(90deg,#ffb347 0%,#e8eef5 55%,#ffffff 100%);
box-shadow: 0 0 6px rgba(255,150,50,.75);
}
.missile::before {
content:"";
position:absolute;
right:100%; top:50%;
width:24px; height:6px;
margin-top:-3px;
background: linear-gradient(90deg, rgba(255,120,0,0) 0%, #ff8c1a 55%, #ffd166 100%);
border-radius:3px 0 0 3px;
transform-origin:100% 50%;
filter: blur(1px);
animation: missileFlame .1s infinite alternate;
}
@keyframes missileFlame {
from { transform: scaleX(.75); opacity:.8; }
to { transform: scaleX(1.15); opacity:1; }
}
.laser-beam {
position:absolute; left:0; top:0;
height:4px;
margin-top:-2px;
transform-origin:0 50%;
border-radius:2px;
background: linear-gradient(90deg, #ffffff 0%, #d8f4ff 18%, #6fc8ff 55%, rgba(79,184,255,0) 100%);
box-shadow: 0 0 14px #4fb8ff, 0 0 34px rgba(79,184,255,.75);
animation: laserFade .34s ease-out forwards;
pointer-events:none;
}
.laser-beam::before {
content:"";
position:absolute;
left:0; top:50%;
width:100%; height:14px;
margin-top:-7px;
border-radius:7px;
background: linear-gradient(90deg, rgba(180,240,255,.55) 0%, rgba(120,210,255,.35) 30%, rgba(79,184,255,0) 100%);
filter: blur(2px);
}
@keyframes laserFade {
0% { opacity:1; }
65% { opacity:1; }
100% { opacity:0; }
}
.muzzle-flash {
position:absolute; left:0; top:0;
width:48px; height:48px;
margin:-24px 0 0 -24px;
border-radius:50%;
background: radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(140,225,255,.65) 38%, rgba(60,180,255,0) 70%);
animation: flashPop .2s ease-out forwards;
pointer-events:none;
}
@keyframes flashPop {
from { opacity:1; transform: scale(.6); }
to { opacity:0; transform: scale(1.3); }
}
/* ===== UI ===== */
.reset-btn {
position:fixed; top:16px; right:16px; z-index:50;
padding:8px 14px;
font-size:12px; letter-spacing:1px;
color:#cfe6ff;
background: rgba(20,40,70,.65);
border:1px solid rgba(100,170,255,.45);
border-radius:8px;
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
cursor:pointer;
-webkit-tap-highlight-color: transparent;
}
.reset-btn:active { background: rgba(40,80,130,.85); }
.hint {
position:fixed; left:50%; bottom:18px;
transform:translateX(-50%);
color: rgba(180,210,240,.55);
font-size:12px; letter-spacing:1.5px;
white-space:nowrap;
pointer-events:none;
z-index:20;
transition: color .3s;
}
.hint.danger { color: #ff8080; }
</style>
</head>
<body>
<div class="viewport" id="viewport">
<div class="scene" id="scene">
<div class="star-layer" id="starLayer"></div>
<div class="station" id="station">
<!-- 太阳能帆板 -->
<div class="panel panel-lt" id="modPanelLt"></div>
<div class="panel panel-lb" id="modPanelLb"></div>
<div class="panel panel-rt" id="modPanelRt"></div>
<div class="panel panel-rb" id="modPanelRb"></div>
<!-- 纵向臂 -->
<div class="mod arm-up" id="modArmUp">
<div class="win" style="left:8px; top:26px;"></div>
<div class="win" style="left:8px; top:48px;"></div>
</div>
<div class="mod arm-down" id="modArmDown">
<div class="win" style="left:8px; top:26px;"></div>
<div class="win" style="left:8px; top:48px;"></div>
</div>
<!-- 横向臂 -->
<div class="mod arm-left" id="modArmLeft">
<div class="win" style="left:28px; top:10px;"></div>
<div class="win" style="left:44px; top:10px;"></div>
</div>
<div class="mod arm-right" id="modArmRight">
<div class="win" style="left:28px; top:10px;"></div>
<div class="win" style="left:44px; top:10px;"></div>
</div>
<!-- 核心舱 -->
<div class="mod core" id="modCore">
<div class="win" style="left:14px; top:10px;"></div>
<div class="win" style="left:78px; top:10px;"></div>
</div>
<!-- 顶部对接口 -->
<div class="port port-v" id="modPort" style="left:215px; top:116px;"></div>
<!-- 停靠飞船 -->
<div class="ship" id="modShip" style="left:50px; top:213px;"></div>
<!-- 底部推进器 -->
<div class="thruster" id="modThruster">
<div class="bell"></div>
<div class="flame">
<div class="flame-glow"></div>
<div class="flame-outer"></div>
<div class="flame-core"></div>
</div>
</div>
<!-- 上方电磁炮 -->
<div class="rail-turret">
<div class="rail-base"></div>
<div class="rail-barrel" id="railBarrel"></div>
</div>
<!-- 核心舱激光炮 -->
<div class="laser-turret">
<div class="laser-base"></div>
<div class="laser-barrel" id="laserBarrel"></div>
</div>
<!-- 左右导弹巢 -->
<div class="missile-pod left" id="podLeft"></div>
<div class="missile-pod right" id="podRight"></div>
</div>
</div>
</div>
<button class="reset-btn" id="resetBtn">重置视角</button>
<div class="hint" id="hint">单指拖动 · 双指缩放 · 点击开火</div>
<script>
(function () {
'use strict';
const viewport = document.getElementById('viewport');
const scene = document.getElementById('scene');
const station = document.getElementById('station');
const starLayer = document.getElementById('starLayer');
const resetBtn = document.getElementById('resetBtn');
const hintEl = document.getElementById('hint');
const railBarrel = document.getElementById('railBarrel');
const laserBarrel = document.getElementById('laserBarrel');
/* ---------- 星空 ---------- */
const frag = document.createDocumentFragment();
for (let i = 0; i < 200; i++) {
const s = document.createElement('div');
s.className = 'star';
const size = (Math.random() * 2 + 1).toFixed(1);
s.style.width = size + 'px';
s.style.height = size + 'px';
s.style.left = (Math.random() * 100).toFixed(2) + '%';
s.style.top = (Math.random() * 100).toFixed(2) + '%';
s.style.animationDelay = (Math.random() * 3).toFixed(2) + 's';
s.style.animationDuration = (2 + Math.random() * 3).toFixed(2) + 's';
frag.appendChild(s);
}
starLayer.appendChild(frag);
/* ---------- 视角状态 ---------- */
const MIN_SCALE = 0.35;
const MAX_SCALE = 3;
let scale = 1, tx = 0, ty = 0;
function apply() {
scene.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`;
}
(function initScale() {
const r = viewport.getBoundingClientRect();
const fit = Math.min(r.width / 560, r.height / 560);
scale = Math.max(MIN_SCALE, Math.min(1, fit));
apply();
})();
/* ---------- 指针手势 ---------- */
const pointers = new Map();
let gestureMulti = false;
let startX = 0, startY = 0, startT = 0;
let pinchDist = 0, pinchCX = 0, pinchCY = 0;
viewport.addEventListener('pointerdown', function (e) {
e.preventDefault();
try { viewport.setPointerCapture(e.pointerId); } catch (err) {}
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
viewport.classList.add('grabbing');
if (pointers.size === 1) {
gestureMulti = false;
startX = e.clientX;
startY = e.clientY;
startT = performance.now();
} else if (pointers.size === 2) {
gestureMulti = true;
const pts = [...pointers.values()];
pinchDist = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
pinchCX = (pts[0].x + pts[1].x) / 2;
pinchCY = (pts[0].y + pts[1].y) / 2;
}
}, { passive: false });
viewport.addEventListener('pointermove', function (e) {
const p = pointers.get(e.pointerId);
if (!p) return;
const dx = e.clientX - p.x;
const dy = e.clientY - p.y;
p.x = e.clientX;
p.y = e.clientY;
if (pointers.size === 1) {
tx += dx; ty += dy;
apply();
return;
}
const pts = [...pointers.values()];
const a = pts[0], b = pts[1];
const dist = Math.hypot(b.x - a.x, b.y - a.y);
const c2x = (a.x + b.x) / 2;
const c2y = (a.y + b.y) / 2;
if (pinchDist > 0 && dist > 0) {
tx += c2x - pinchCX;
ty += c2y - pinchCY;
const rect = viewport.getBoundingClientRect();
const cX = rect.width / 2;
const cY = rect.height / 2;
const mx = c2x - rect.left;
const my = c2y - rect.top;
let ns = scale * (dist / pinchDist);
ns = Math.max(MIN_SCALE, Math.min(MAX_SCALE, ns));
if (ns !== scale) {
const k = ns / scale;
const ux = mx - cX;
const uy = my - cY;
tx = ux * (1 - k) + k * tx;
ty = uy * (1 - k) + k * ty;
scale = ns;
}
apply();
}
pinchDist = dist;
pinchCX = c2x;
pinchCY = c2y;
}, { passive: false });
function endPointer(e) {
if (!pointers.has(e.pointerId)) return;
const wasSingle = (pointers.size === 1);
pointers.delete(e.pointerId);
if (pointers.size === 0) viewport.classList.remove('grabbing');
if (wasSingle && !gestureMulti) {
const dt = performance.now() - startT;
const dist = Math.hypot(e.clientX - startX, e.clientY - startY);
if (dist < 12 && dt < 400) {
fireAll(e.clientX, e.clientY);
}
}
if (pointers.size === 0) {
gestureMulti = false;
pinchDist = 0;
} else if (pointers.size === 1) {
pinchDist = 0;
}
}
viewport.addEventListener('pointerup', endPointer);
viewport.addEventListener('pointercancel', endPointer);
/* ============================================================
模块完整度系统
============================================================ */
const STATION_ORIGIN = { x: 380, y: 380 };
let off = { x: 0, y: 0 }; // 空间站浮动偏移
const MODULE_DEFS = [
{ id:'panel-lt', sel:'#modPanelLt', x:79, y:136, w:26, h:72, hp:40, fragColor:'#3a7bc8' },
{ id:'panel-lb', sel:'#modPanelLb', x:79, y:232, w:26, h:72, hp:40, fragColor:'#3a7bc8' },
{ id:'panel-rt', sel:'#modPanelRt', x:335, y:136, w:26, h:72, hp:40, fragColor:'#3a7bc8' },
{ id:'panel-rb', sel:'#modPanelRb', x:335, y:232, w:26, h:72, hp:40, fragColor:'#3a7bc8' },
{ id:'arm-up', sel:'#modArmUp', x:208, y:128, w:24, h:80, hp:90, fragColor:'#e8eef5', weaponSel:'.rail-turret' },
{ id:'arm-down', sel:'#modArmDown', x:208, y:232, w:24, h:80, hp:90, fragColor:'#e8eef5' },
{ id:'arm-left', sel:'#modArmLeft', x:92, y:208, w:80, h:24, hp:90, fragColor:'#e8eef5', weaponSel:'.missile-pod.left' },
{ id:'arm-right', sel:'#modArmRight', x:268, y:208, w:80, h:24, hp:90, fragColor:'#e8eef5', weaponSel:'.missile-pod.right' },
{ id:'core', sel:'#modCore', x:172, y:208, w:96, h:24, hp:160, fragColor:'#e8eef5', weaponSel:'.laser-turret' },
{ id:'port', sel:'#modPort', x:215, y:116, w:10, h:12, hp:30, fragColor:'#b9c6d4' },
{ id:'ship', sel:'#modShip', x:50, y:213, w:30, h:14, hp:50, fragColor:'#e8eef5' },
{ id:'thruster', sel:'#modThruster', x:206, y:324, w:28, h:14, hp:60, fragColor:'#b7c4d3' }
];
const modules = [];
const moduleMap = new Map();
function initModules() {
MODULE_DEFS.forEach(function (def) {
const el = document.querySelector(def.sel);
if (!el) return;
// 创建血条
const bar = document.createElement('div');
bar.className = 'hp-bar';
const bw = Math.max(def.w, 30);
bar.style.width = bw + 'px';
bar.style.left = (def.x + def.w / 2 - bw / 2) + 'px';
bar.style.top = (def.y - 9) + 'px';
const fill = document.createElement('div');
fill.className = 'hp-fill';
bar.appendChild(fill);
station.appendChild(bar);
const m = {
id: def.id,
el: el,
bar: bar,
fill: fill,
weaponSel: def.weaponSel || null,
x: def.x, y: def.y, w: def.w, h: def.h,
fragColor: def.fragColor || '#e8eef5',
maxHp: def.hp,
hp: def.hp,
alive: true,
hideTimer: null
};
modules.push(m);
moduleMap.set(m.id, m);
});
}
initModules();
function isAlive(id) {
const m = moduleMap.get(id);
return !!(m && m.alive);
}
function moduleWorldRect(m) {
return {
x: STATION_ORIGIN.x + off.x + m.x,
y: STATION_ORIGIN.y + off.y + m.y,
w: m.w,
h: m.h
};
}
function updateHpBar(m) {
const ratio = Math.max(0, m.hp / m.maxHp);
m.fill.style.width = (ratio * 100) + '%';
let c;
if (ratio > 0.6) c = '#4ade80';
else if (ratio > 0.3) c = '#facc15';
else c = '#f87171';
m.fill.style.background = c;
m.fill.style.boxShadow = '0 0 6px ' + c;
}
function showHpBar(m) {
if (!m.alive) return;
m.bar.style.opacity = 1;
clearTimeout(m.hideTimer);
m.hideTimer = setTimeout(function () {
if (m.alive) m.bar.style.opacity = 0;
}, 2600);
}
function damageModule(m, dmg) {
if (!m.alive) return;
m.hp -= dmg;
if (m.hp <= 0) {
m.hp = 0;
updateHpBar(m);
shatterModule(m);
return;
}
updateHpBar(m);
showHpBar(m);
}
/* ---------- 碎裂 ---------- */
function shatterModule(m) {
m.alive = false;
clearTimeout(m.hideTimer);
m.el.style.display = 'none';
if (m.weaponSel) {
const w = station.querySelector(m.weaponSel);
if (w) w.style.display = 'none';
}
m.bar.style.opacity = 0;
setTimeout(function () { if (m.bar.parentNode) m.bar.remove(); }, 500);
const rect = moduleWorldRect(m);
spawnFragments(rect, m.fragColor);
// 中心闪光
spawnFlash(rect.x + rect.w / 2, rect.y + rect.h / 2, 'boom');
if (m.id === 'core') {
hintEl.textContent = '⚠ 核心舱损毁 · 空间站失去控制';
hintEl.classList.add('danger');
}
}
function spawnFragments(rect, color) {
const cols = Math.max(2, Math.round(rect.w / 22));
const rows = Math.max(2, Math.round(rect.h / 18));
const cw = rect.w / cols;
const ch = rect.h / rows;
const total = Math.min(cols * rows, 24);
for (let i = 0; i < total; i++) {
const c = i % cols;
const r = Math.floor(i / cols);
const fx = rect.x + c * cw;
const fy = rect.y + r * ch;
const el = document.createElement('div');
el.className = 'fragment';
el.style.left = fx + 'px';
el.style.top = fy + 'px';
el.style.width = (cw * 0.82) + 'px';
el.style.height = (ch * 0.82) + 'px';
el.style.background = color;
el.style.boxShadow = '0 0 8px rgba(200,230,255,.7)';
const ang = Math.random() * Math.PI * 2;
const dist = 30 + Math.random() * 80;
el.style.setProperty('--dx', (Math.cos(ang) * dist).toFixed(1) + 'px');
el.style.setProperty('--dy', (Math.sin(ang) * dist).toFixed(1) + 'px');
el.style.setProperty('--rot', (Math.random() * 720 - 360).toFixed(0) + 'deg');
el.style.animationDelay = (Math.random() * 0.08).toFixed(2) + 's';
scene.appendChild(el);
setTimeout(function () { el.remove(); }, 1150);
}
}
function spawnParticles(x, y, count, color, baseSize) {
for (let i = 0; i < count; i++) {
const el = document.createElement('div');
el.className = 'fragment';
const s = baseSize * (0.5 + Math.random());
el.style.width = s + 'px';
el.style.height = s + 'px';
el.style.borderRadius = '50%';
el.style.background = color;
el.style.left = (x - s / 2) + 'px';
el.style.top = (y - s / 2) + 'px';
el.style.boxShadow = '0 0 8px ' + color;
const ang = Math.random() * Math.PI * 2;
const dist = 20 + Math.random() * 60;
el.style.setProperty('--dx', (Math.cos(ang) * dist).toFixed(1) + 'px');
el.style.setProperty('--dy', (Math.sin(ang) * dist).toFixed(1) + 'px');
el.style.setProperty('--rot', '0deg');
scene.appendChild(el);
setTimeout(function () { el.remove(); }, 1000);
}
}
/* ============================================================
陨石系统
============================================================ */
const meteors = [];
let meteorTimer = 4; // 开局 4 秒安全期
let elapsed = 0;
function spawnMeteor() {
const ang = Math.random() * Math.PI * 2;
const dist = 620 + Math.random() * 260;
const x = 600 + Math.cos(ang) * dist;
const y = 600 + Math.sin(ang) * dist;
// 瞄准空间站中心附近
const tx = STATION_ORIGIN.x + off.x + 220 + (Math.random() - 0.5) * 130;
const ty = STATION_ORIGIN.y + off.y + 220 + (Math.random() - 0.5) * 130;
const a = Math.atan2(ty - y, tx - x);
const speed = 80 + Math.random() * 80;
const r = 7 + Math.random() * 8;
const el = document.createElement('div');
el.className = 'meteor';
el.style.width = (r * 2) + 'px';
el.style.height = (r * 2) + 'px';
scene.appendChild(el);
meteors.push({
el: el,
x: x, y: y, r: r,
vx: Math.cos(a) * speed,
vy: Math.sin(a) * speed,
rot: Math.random() * 360,
spin: (Math.random() - 0.5) * 2.5
});
}
function destroyMeteor(index) {
const mt = meteors[index];
if (!mt) return;
spawnParticles(mt.x, mt.y, 7, '#ffb060', 4);
spawnParticles(mt.x, mt.y, 4, '#ffffff', 3);
mt.el.remove();
meteors.splice(index, 1);
}
function circleRectHit(cx, cy, r, rx, ry, rw, rh) {
const nx = Math.max(rx, Math.min(cx, rx + rw));
const ny = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - nx;
const dy = cy - ny;
return dx * dx + dy * dy <= r * r;
}
function updateMeteors(dt) {
elapsed += dt;
// 难度随时间提升
meteorTimer -= dt;
if (meteorTimer <= 0) {
const interval = Math.max(0.9, 2.4 - elapsed * 0.018);
meteorTimer = interval * (0.7 + Math.random() * 0.6);
if (meteors.length < 24) spawnMeteor();
}
for (let i = meteors.length - 1; i >= 0; i--) {
const mt = meteors[i];
mt.x += mt.vx * dt;
mt.y += mt.vy * dt;
mt.rot += mt.spin * dt * 60;
mt.el.style.transform =
`translate(${(mt.x - mt.r).toFixed(1)}px, ${(mt.y - mt.r).toFixed(1)}px) rotate(${mt.rot.toFixed(0)}deg)`;
// 与模块碰撞
let hit = false;
for (let k = 0; k < modules.length; k++) {
const m = modules[k];
if (!m.alive) continue;
const rc = moduleWorldRect(m);
if (circleRectHit(mt.x, mt.y, mt.r, rc.x, rc.y, rc.w, rc.h)) {
damageModule(m, 12 + Math.random() * 10);
hit = true;
break;
}
}
if (hit) { destroyMeteor(i); continue; }
// 飞出边界
if (mt.x < -500 || mt.x > 1700 || mt.y < -500 || mt.y > 1700) {
mt.el.remove();
meteors.splice(i, 1);
}
}
}
/* ============================================================
武器系统
============================================================ */
const projectiles = [];
function getStationOffset() {
const t = getComputedStyle(station).transform;
if (t && t !== 'none') {
const m = t.match(/matrix\(([^)]+)\)/);
if (m) {
const v = m[1].split(',').map(Number);
return { x: v[4], y: v[5] };
}
}
return { x: 0, y: 0 };
}
function screenToScene(clientX, clientY) {
const rect = viewport.getBoundingClientRect();
const cX = rect.width / 2;
const cY = rect.height / 2;
const mx = clientX - rect.left;
const my = clientY - rect.top;
return {
x: (mx - cX - tx) / scale,
y: (my - cY - ty) / scale
};
}
function fireAll(clientX, clientY) {
const target = screenToScene(clientX, clientY);
const O = {
x: STATION_ORIGIN.x + off.x,
y: STATION_ORIGIN.y + off.y
};
// 电磁炮 → 需要上纵臂存活
if (isAlive('arm-up')) {
fireRailgun(O.x + 220, O.y + 122, target.x, target.y);
}
// 激光 → 需要核心舱存活
if (isAlive('core')) {
fireLaser(O.x + 220, O.y + 220, target.x, target.y);
}
// 导弹 → 左右横臂各自存活
if (isAlive('arm-left')) {
fireMissile(O.x + 90, O.y + 220, target.x, target.y);
}
if (isAlive('arm-right')) {
fireMissile(O.x + 350, O.y + 220, target.x, target.y);
}
}
function fireRailgun(bx, by, tx, ty) {
const angle = Math.atan2(ty - by, tx - bx);
railBarrel.style.transform = `rotate(${angle}rad)`;
const len = 56;
const mx = bx + Math.cos(angle) * len;
const my = by + Math.sin(angle) * len;
const el = document.createElement('div');
el.className = 'projectile rail-slug';
scene.appendChild(el);
projectiles.push({
el: el, x: mx, y: my, angle: angle,
vx: Math.cos(angle) * 2200,
vy: Math.sin(angle) * 2200,
life: 0, maxLife: 1.4,
radius: 9
});
spawnFlash(mx, my, 'rail');
}
function fireLaser(bx, by, tx, ty) {
const angle = Math.atan2(ty - by, tx - bx);
laserBarrel.style.transform = `rotate(${angle}rad)`;
const len0 = 30;
const mx = bx + Math.cos(angle) * len0;
const my = by + Math.sin(angle) * len0;
const dist = Math.hypot(tx - mx, ty - my) + 320;
const beam = document.createElement('div');
beam.className = 'laser-beam';
beam.style.width = dist + 'px';
beam.style.transform = `translate(${mx}px, ${my}px) rotate(${angle}rad)`;
scene.appendChild(beam);
setTimeout(function () { beam.remove(); }, 400);
spawnFlash(mx, my, 'laser');
// 激光沿途击毁陨石
laserRayHit(mx, my, angle, Math.min(dist, 1400));
}
function laserRayHit(x0, y0, angle, maxDist) {
const cosA = Math.cos(angle);
const sinA = Math.sin(angle);
const hits = [];
for (let j = 0; j < meteors.length; j++) {
const mt = meteors[j];
const dx = mt.x - x0;
const dy = mt.y - y0;
const proj = dx * cosA + dy * sinA;
if (proj <= 0 || proj > maxDist) continue;
const perp = Math.abs(-dx * sinA + dy * cosA);
if (perp < mt.r + 10) hits.push(j);
}
hits.sort(function (a, b) { return b - a; });
hits.forEach(function (j) { destroyMeteor(j); });
}
function fireMissile(bx, by, tx, ty) {
const angle = Math.atan2(ty - by, tx - bx);
const el = document.createElement('div');
el.className = 'projectile missile';
el.style.transform = `translate(${bx}px, ${by}px) rotate(${angle}rad)`;
scene.appendChild(el);
projectiles.push({
el: el, x: bx, y: by, angle: angle,
vx: Math.cos(angle) * 680,
vy: Math.sin(angle) * 680,
life: 0, maxLife: 2.4,
radius: 8
});
spawnFlash(bx, by, 'missile');
}
function spawnFlash(x, y, type) {
const el = document.createElement('div');
el.className = 'muzzle-flash';
el.style.transform = `translate(${x}px, ${y}px)`;
if (type === 'missile') {
el.style.background = 'radial-gradient(circle, rgba(255,240,200,.95) 0%, rgba(255,160,60,.7) 35%, rgba(255,120,0,0) 70%)';
} else if (type === 'rail') {
el.style.background = 'radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(140,230,255,.7) 35%, rgba(80,190,255,0) 70%)';
} else if (type === 'boom') {
el.style.width = '90px';
el.style.height = '90px';
el.style.margin = '-45px 0 0 -45px';
el.style.background = 'radial-gradient(circle, rgba(255,255,255,.95) 0%, rgba(255,180,80,.75) 35%, rgba(255,120,0,0) 70%)';
}
scene.appendChild(el);
setTimeout(function () { el.remove(); }, 260);
}
/* ---------- 弹丸更新 + 碰撞 ---------- */
function updateProjectiles(dt) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life += dt;
let removed = false;
// 与陨石碰撞
for (let j = meteors.length - 1; j >= 0; j--) {
const mt = meteors[j];
const dx = p.x - mt.x;
const dy = p.y - mt.y;
if (dx * dx + dy * dy < (mt.r + p.radius) * (mt.r + p.radius)) {
destroyMeteor(j);
p.el.remove();
projectiles.splice(i, 1);
removed = true;
break;
}
}
if (removed) continue;
// 生命周期 / 边界
if (p.life > p.maxLife ||
p.x < -800 || p.x > 2000 ||
p.y < -800 || p.y > 2000) {
p.el.remove();
projectiles.splice(i, 1);
continue;
}
p.el.style.transform =
`translate(${p.x.toFixed(1)}px, ${p.y.toFixed(1)}px) rotate(${p.angle}rad)`;
}
}
/* ============================================================
主循环
============================================================ */
let lastT = performance.now();
function loop(now) {
const dt = Math.min((now - lastT) / 1000, 0.05);
lastT = now;
off = getStationOffset();
updateMeteors(dt);
updateProjectiles(dt);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* ---------- 重置视角 ---------- */
resetBtn.addEventListener('click', function () {
const r = viewport.getBoundingClientRect();
const fit = Math.min(r.width / 560, r.height / 560);
const targetScale = Math.max(MIN_SCALE, Math.min(1, fit));
scene.style.transition = 'transform .3s ease-out';
scale = targetScale;
tx = 0;
ty = 0;
apply();
setTimeout(function () { scene.style.transition = ''; }, 320);
});
})();
</script>
</body>
</html>Game Source: 模块化空间站 · 完整度系统
Creator: ApexFalcon66
Libraries: none
Complexity: complex (1104 lines, 33.8 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-apexfalcon66-mvdgpz82" to link back to the original. Then publish at arcadelab.ai/publish.