深空舰队 - 极简战术版
by CosmicRider48676 lines34.4 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>
body { margin: 0; overflow: hidden; background: #03050a; font-family: monospace; touch-action: none; }
canvas { display: block; width: 100vw; height: 100vh; }
/* 配置界面 */
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background: rgba(3, 5, 10, 0.9); z-index: 10; }
.panel { background: rgba(15, 25, 45, 0.95); border: 1px solid #3a5a8a; padding: 15px; border-radius: 8px; color: #fff; width: 90%; max-width: 600px; }
h1 { text-align: center; font-size: 18px; color: #66ccff; margin: 0 0 15px 0; }
.fleet-config { display: flex; justify-content: space-between; margin-bottom: 15px; }
.side { width: 48%; }
.side h3 { font-size: 13px; margin: 0 0 8px 0; color: #a0b0d0; border-bottom: 1px solid #3a5a8a; padding-bottom: 4px; text-align: center;}
.row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 11px; }
.btn-group button { width: 20px; height: 20px; background: #2a4a7a; border: none; color: #fff; border-radius: 3px; cursor: pointer; }
.val { width: 16px; text-align: center; font-weight: bold; color: #66ccff;}
#start-btn { width: 100%; padding: 10px; background: #0055ff; color: #fff; border: none; border-radius: 4px; font-size: 15px; font-weight: bold; cursor: pointer; }
/* 极简战术信息面板 */
#tactical-ui {
position: absolute; top: 10px; left: 10px;
color: #fff; display: none;
background: rgba(5, 10, 20, 0.65); /* 更透明 */
border-left: 3px solid #66ccff;
padding: 8px 12px; border-radius: 2px;
font-size: 11px; pointer-events: none;
line-height: 1.4; box-shadow: 0 0 10px rgba(0,0,0,0.3);
letter-spacing: 0.5px;
}
.tac-title { font-size: 13px; font-weight: bold; color: #66ccff; margin-bottom: 4px; }
.tac-captain { color: #ffaa00; }
.tac-data { color: #a0b0d0; }
.tac-modules { margin-top: 5px; color: #00ff88; display: flex; flex-wrap: wrap; gap: 6px; max-width: 250px;}
.mod-danger { color: #ff0044; }
.log { position: absolute; bottom: 10px; left: 10px; font-size: 11px; color: #aaa; max-width: 300px; text-shadow: 1px 1px 2px #000; pointer-events: none;}
</style>
</head>
<body>
<div id="ui-layer">
<div class="panel">
<h1>深空舰队配置</h1>
<div class="fleet-config">
<div class="side">
<h3 style="color:#66ccff">蓝方</h3>
<div class="row"><span>激光舰</span> <div class="btn-group"><button onclick="updateCount('bLaser',-1)">-</button><span class="val" id="val-bLaser">2</span><button onclick="updateCount('bLaser',1)">+</button></div></div>
<div class="row"><span>突击舰</span> <div class="btn-group"><button onclick="updateCount('bAssault',-1)">-</button><span class="val" id="val-bAssault">1</span><button onclick="updateCount('bAssault',1)">+</button></div></div>
<div class="row"><span>导弹舰</span> <div class="btn-group"><button onclick="updateCount('bMissile',-1)">-</button><span class="val" id="val-bMissile">0</span><button onclick="updateCount('bMissile',1)">+</button></div></div>
<div class="row"><span>补给舰</span> <div class="btn-group"><button onclick="updateCount('bSupply',-1)">-</button><span class="val" id="val-bSupply">0</span><button onclick="updateCount('bSupply',1)">+</button></div></div>
</div>
<div class="side">
<h3 style="color:#ff3366">红方</h3>
<div class="row"><span>激光舰</span> <div class="btn-group"><button onclick="updateCount('rLaser',-1)">-</button><span class="val" id="val-rLaser">0</span><button onclick="updateCount('rLaser',1)">+</button></div></div>
<div class="row"><span>突击舰</span> <div class="btn-group"><button onclick="updateCount('rAssault',-1)">-</button><span class="val" id="val-rAssault">0</span><button onclick="updateCount('rAssault',1)">+</button></div></div>
<div class="row"><span>导弹舰</span> <div class="btn-group"><button onclick="updateCount('rMissile',-1)">-</button><span class="val" id="val-rMissile">2</span><button onclick="updateCount('rMissile',1)">+</button></div></div>
<div class="row"><span>补给舰</span> <div class="btn-group"><button onclick="updateCount('rSupply',-1)">-</button><span class="val" id="val-rSupply">1</span><button onclick="updateCount('rSupply',1)">+</button></div></div>
</div>
</div>
<button id="start-btn" onclick="startBattle()">开 战</button>
</div>
</div>
<div id="tactical-ui"></div>
<div class="log" id="log"></div>
<canvas id="gameCanvas"></canvas>
<script>
// ================= 基础设定 =================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W = window.innerWidth, H = window.innerHeight;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
window.addEventListener('resize', resize); resize();
const WORLD_W = 8000; const WORLD_H = 6000;
let cam = { x: WORLD_W/2, y: WORLD_H/2, zoom: 0.15 };
let selectedShip = null;
function dist(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); }
function angleTo(a, b) { return Math.atan2(b.y - a.y, b.x - a.x); }
function normalizeAngle(a) { while (a > Math.PI) a -= Math.PI*2; while (a < -Math.PI) a += Math.PI*2; return a; }
function logMsg(msg) { document.getElementById('log').innerHTML = `> ${msg}<br>` + document.getElementById('log').innerHTML.slice(0, 100); }
const config = { bLaser: 2, bAssault: 1, bMissile: 0, bSupply: 0, rLaser: 0, rAssault: 0, rMissile: 2, rSupply: 1 };
function updateCount(type, delta) { config[type] = Math.max(0, config[type] + delta); document.getElementById('val-' + type).innerText = config[type]; }
let entities = [], bullets = [], particles = [];
let gameState = 'config';
// 随机名字库
const namePrefixes = ['星辰', '烈焰', '风暴', '虚空', '苍穹', '极光', '深渊', '破晓', '神盾', '利刃'];
const nameSuffixes = ['号', '之翼', '守望者', '先锋', '之盾', '幽影', '脉冲', '远征'];
const captainNames = ['张伟', '李强', '王磊', '刘洋', '陈杰', '赵明', '林奇', '周鑫', '孙宇', '高飞'];
function getRandomName() { return namePrefixes[Math.floor(Math.random()*namePrefixes.length)] + nameSuffixes[Math.floor(Math.random()*nameSuffixes.length)]; }
function getRandomCaptain() { return captainNames[Math.floor(Math.random()*captainNames.length)] + ' 舰长'; }
// ================= 触屏交互 =================
let isDragging = false, lastTouchDist = 0;
let touchStart = { x: 0, y: 0 }, camStart = { x: 0, y: 0 };
let pointerDownTime = 0, movedDist = 0;
canvas.addEventListener('pointerdown', (e) => {
if (gameState !== 'playing') return;
isDragging = true; pointerDownTime = Date.now(); movedDist = 0;
touchStart = { x: e.clientX, y: e.clientY };
camStart = { x: cam.x, y: cam.y };
});
canvas.addEventListener('pointermove', (e) => {
if (!isDragging || gameState !== 'playing') return;
const dx = e.clientX - touchStart.x, dy = e.clientY - touchStart.y;
movedDist = Math.hypot(dx, dy);
if (!selectedShip) { // 如果锁定了目标,就不能拖拽视角
cam.x = camStart.x - dx / cam.zoom;
cam.y = camStart.y - dy / cam.zoom;
}
});
canvas.addEventListener('pointerup', (e) => {
if (gameState !== 'playing') return;
isDragging = false;
if (movedDist < 10 && Date.now() - pointerDownTime < 300) {
handleClick(e.clientX, e.clientY);
}
});
canvas.addEventListener('touchmove', (e) => {
if (gameState !== 'playing') return;
if (e.touches.length === 2) {
e.preventDefault();
const currentDist = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
if (lastTouchDist === 0) lastTouchDist = currentDist;
cam.zoom = Math.max(0.05, Math.min(cam.zoom * (currentDist / lastTouchDist), 1.5));
lastTouchDist = currentDist;
}
}, { passive: false });
canvas.addEventListener('touchend', (e) => { if (e.touches.length < 2) lastTouchDist = 0; });
function handleClick(screenX, screenY) {
const worldX = (screenX - W / 2) / cam.zoom + cam.x;
const worldY = (screenY - H / 2) / cam.zoom + cam.y;
let clickedShip = null;
for (let i = entities.length - 1; i >= 0; i--) {
const e = entities[i];
if (!e.dead && Math.hypot(worldX - e.x, worldY - e.y) < e.radius + 50) {
clickedShip = e; break;
}
}
// 如果点击了空白区域,取消选中
selectedShip = clickedShip;
updateTacticalUI();
}
// ================= 极简战术 UI 更新 =================
const uiEl = document.getElementById('tactical-ui');
function updateTacticalUI() {
if (!selectedShip || selectedShip.dead) {
uiEl.style.display = 'none';
if (selectedShip && selectedShip.dead) selectedShip = null;
return;
}
uiEl.style.display = 'block';
const s = selectedShip;
// 舰长状态计算
const coreMod = s.modules.find(m => m.type === 'core');
let captainStatus = '<span style="color:#00ff88">正常</span>';
if (coreMod) {
if (coreMod.hp <= 0) captainStatus = '<span style="color:#ff0044">阵亡</span>';
else if (coreMod.hp <= 20) captainStatus = '<span style="color:#ff0044">重伤</span>';
else if (coreMod.hp <= 50) captainStatus = '<span style="color:#ffaa00">轻伤</span>';
}
// 基础信息
let html = `<div class="tac-title">${s.name} <span style="float:right; color:#aaa; font-size:10px;">${s.isEnemy ? '红方' : '蓝方'}</span></div>`;
html += `<div class="tac-captain">${s.captain} [${captainStatus}]</div>`;
// 弹药/乘员 (紧凑排布)
html += `<div class="tac-data" style="margin-top:4px;">`;
if (s.ammo !== undefined) html += `导弹:${Math.floor(s.ammo)} `;
if (s.railgunAmmo !== undefined) html += `电磁:${s.railgunAmmo} `;
html += `| 乘员:${s.crew.filter(c => c.pos !== -1).length}/${s.crew.length}</div>`;
// 模块状态 (只显示活着的,用纯数字百分比)
html += `<div class="tac-modules">`;
s.modules.forEach((m, i) => {
if (m.hp > 0) {
const pct = Math.floor(m.hp);
// 清理名字,只留前两个字,方便排版
let typeName = m.type.replace('weapon_', '').substring(0, 2);
let colorClass = pct <= 20 ? 'mod-danger' : '';
html += `<span class="${colorClass}">${typeName}[${pct}%]</span>`;
}
});
html += `</div>`;
uiEl.innerHTML = html;
}
// ================= 渲染模块 =================
const MOD_W = 45, MOD_H = 26;
function drawTurret(ctx, x, y, angle, type) {
ctx.save(); ctx.translate(x, y); ctx.rotate(angle);
if (type === 'railgun') {
ctx.fillStyle = '#2a3b4a'; ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#5c738a'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.fillStyle = '#111'; ctx.fillRect(4, -5, 22, 4); ctx.fillRect(4, 1, 22, 4);
ctx.fillStyle = '#00ffcc'; ctx.fillRect(20, -5, 6, 4); ctx.fillRect(20, 1, 6, 4);
} else if (type === 'pd') {
ctx.fillStyle = '#3b3b3b'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#888'; ctx.fillRect(3, -3, 10, 6);
} else if (type === 'laser_intercept') {
ctx.fillStyle = '#1e2b38'; ctx.beginPath(); ctx.arc(0, 0, 5, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#66ccff'; ctx.lineWidth = 1; ctx.stroke();
ctx.fillStyle = '#111'; ctx.fillRect(3, -1.5, 10, 3);
ctx.fillStyle = '#66ccff'; ctx.beginPath(); ctx.arc(13, 0, 2, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
function drawFixedMainCannon(ctx, x, y, isOverheated, broken) {
ctx.save(); ctx.translate(x, y);
ctx.fillStyle = broken ? '#111' : (isOverheated ? '#552222' : '#1e2b38');
ctx.beginPath(); ctx.moveTo(-10, -12); ctx.lineTo(15, -8); ctx.lineTo(15, 8); ctx.lineTo(-10, 12); ctx.closePath(); ctx.fill();
if(!broken) {
ctx.strokeStyle = '#4a6b8c'; ctx.lineWidth = 1.5; ctx.stroke();
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(15, -6, 35, 4); ctx.fillRect(15, 2, 35, 4);
if (!isOverheated) {
ctx.fillStyle = '#66ccff'; ctx.fillRect(15, -3, 35, 2);
ctx.beginPath(); ctx.arc(50, -4, 3, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(50, 4, 3, 0, Math.PI*2); ctx.fill();
}
}
ctx.restore();
}
function drawFire(ctx, x, y) {
const t = Date.now() * 0.01;
for(let i=0; i<3; i++) {
const size = Math.random() * 8 + 4;
ctx.fillStyle = Math.random() > 0.5 ? 'rgba(255, 100, 0, 0.8)' : 'rgba(255, 200, 0, 0.8)';
ctx.beginPath(); ctx.arc(x + (Math.random()-0.5)*20, y + (Math.random()-0.5)*10 - Math.sin(t+i)*5, size, 0, Math.PI*2); ctx.fill();
}
}
function drawModule(ctx, module, x, y, isLast) {
ctx.save(); ctx.translate(x, y);
const isBroken = module.hp <= 0;
const isCrit = module.hp > 0 && module.hp <= 20;
if (module.type === 'solar' || module.type === 'radiator') {
if (isBroken || isCrit) {
ctx.fillStyle = '#222'; ctx.fillRect(6, -28, MOD_W-12, 28); ctx.fillRect(6, MOD_H, MOD_W-12, 28);
if (!isBroken) { ctx.fillStyle = 'rgba(255,50,50,0.3)'; ctx.fillRect(6, -28, MOD_W-12, 28); ctx.fillRect(6, MOD_H, MOD_W-12, 28); }
} else if (module.type === 'solar') {
ctx.fillStyle = '#1a2b4c'; ctx.strokeStyle = '#4a7aab'; ctx.lineWidth = 1;
ctx.fillRect(6, -28, MOD_W-12, 28); ctx.strokeRect(6, -28, MOD_W-12, 28);
ctx.fillRect(6, MOD_H, MOD_W-12, 28); ctx.strokeRect(6, MOD_H, MOD_W-12, 28);
ctx.strokeStyle = 'rgba(102, 204, 255, 0.3)';
for(let i=6; i<MOD_W-6; i+=8) { ctx.beginPath(); ctx.moveTo(i, -28); ctx.lineTo(i, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(i, MOD_H); ctx.lineTo(i, MOD_H+28); ctx.stroke(); }
} else {
ctx.fillStyle = '#331111'; ctx.fillRect(8, -22, MOD_W-16, 22); ctx.fillRect(8, MOD_H, MOD_W-16, 22);
if(isCrit) { drawFire(ctx, MOD_W/2, -10); drawFire(ctx, MOD_W/2, MOD_H+10); }
}
ctx.fillStyle = '#555'; ctx.fillRect(MOD_W/2-2, -5, 4, MOD_H+10);
ctx.restore(); return;
}
let baseColor = (isBroken || isCrit) ? '#111' : '#2c3e50', highlight = (isBroken || isCrit) ? '#222' : '#4a6b8c', shadow = (isBroken || isCrit) ? '#000' : '#1a252f';
if (module.type === 'engine' && !isBroken && !isCrit) { baseColor = '#3d2b2b'; highlight = '#6b4c4c'; }
if (module.type.includes('weapon') && !isBroken && !isCrit) { baseColor = '#1a2b3b'; highlight = '#2e4a6b'; }
const chamfer = 4;
ctx.beginPath(); ctx.moveTo(chamfer, 0); ctx.lineTo(MOD_W - chamfer, 0); ctx.lineTo(MOD_W, chamfer);
ctx.lineTo(MOD_W, MOD_H - chamfer); ctx.lineTo(MOD_W - chamfer, MOD_H); ctx.lineTo(chamfer, MOD_H);
ctx.lineTo(0, MOD_H - chamfer); ctx.lineTo(0, chamfer); ctx.closePath();
const grad = ctx.createLinearGradient(0, 0, 0, MOD_H);
grad.addColorStop(0, highlight); grad.addColorStop(0.5, baseColor); grad.addColorStop(1, shadow);
ctx.fillStyle = grad; ctx.fill();
ctx.strokeStyle = (isBroken || isCrit) ? '#000' : '#050505'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, MOD_H); ctx.stroke();
if (isLast) { ctx.beginPath(); ctx.moveTo(MOD_W, 0); ctx.lineTo(MOD_W, MOD_H); ctx.stroke(); }
if (!isBroken && !isCrit) {
if (module.type === 'engine') {
const flameLen = 15 + Math.random() * 10;
const g = ctx.createLinearGradient(0, MOD_H/2, -flameLen, MOD_H/2);
g.addColorStop(0, 'rgba(102, 204, 255, 0.9)'); g.addColorStop(1, 'rgba(0, 100, 255, 0)');
ctx.fillStyle = g; ctx.beginPath(); ctx.moveTo(0, 6); ctx.lineTo(-flameLen, MOD_H/2); ctx.lineTo(0, MOD_H-6); ctx.fill();
}
else if (module.type === 'weapon_laser') { drawFixedMainCannon(ctx, MOD_W - 5, MOD_H/2, module.isOverheated, false); }
else if (module.type === 'supply') {
ctx.fillStyle = '#888'; ctx.fillRect(MOD_W/2 - 6, -10, 12, 10); ctx.fillRect(MOD_W/2 - 6, MOD_H, 12, 10); ctx.fillRect(MOD_W, MOD_H/2 - 6, 10, 12);
ctx.fillStyle = '#0f0'; ctx.fillRect(MOD_W/2 - 4, -10, 8, 2); ctx.fillRect(MOD_W/2 - 4, MOD_H+8, 8, 2); ctx.fillRect(MOD_W+8, MOD_H/2 - 4, 2, 8);
}
else if (module.type === 'core') { ctx.fillStyle = 'rgba(102, 204, 255, 0.4)'; ctx.fillRect(10, 8, 25, 10); }
}
if (isCrit) { drawFire(ctx, MOD_W/2, MOD_H/2); }
// 去掉了舱室上的微型血条,画面更干净
ctx.restore();
}
// ================= 实体与战斗逻辑 =================
class Ship {
constructor(x, y, isEnemy, color, modulesData, extrasData) {
this.x = x; this.y = y; this.isEnemy = isEnemy; this.color = color;
this.name = getRandomName();
this.captain = getRandomCaptain();
this.vx = 0; this.vy = 0; this.angle = isEnemy ? Math.PI : 0;
this.maxSpeed = 3.0; this.turnSpeed = 0.04; this.radius = 30; this.dead = false;
this.target = null; this.turretAngle = 0;
this.interceptCd = 0;
this.orbitDir = Math.random() > 0.5 ? 1 : -1;
this.modules = modulesData.map(type => ({ type, hp: 100, maxHp: 100, isOverheated: false }));
this.totalWidth = this.modules.length * MOD_W;
this.extras = extrasData || [];
this.crew = [{ id: 0, role: 'captain', pos: 'core' }];
for(let i=1; i<8; i++) this.crew.push({ id: i, role: 'engineer', targetIndex: null, progress: 0, pos: Math.floor(Math.random() * this.modules.length) });
}
isModuleFunctional(type) {
const mods = this.modules.filter(m => m.type === type);
if (mods.length === 0) return false;
return mods.some(m => m.hp > 20);
}
takeDamage(hitX, hitY, damage) {
if (this.dead) return;
const cos = Math.cos(-this.angle), sin = Math.sin(-this.angle);
const localX = (hitX - this.x) * cos - (hitY - this.y) * sin;
const localY = (hitX - this.x) * sin + (hitY - this.y) * cos;
if (Math.abs(localY) > MOD_H / 2 + 5) return;
const index = Math.floor((localX - (-this.totalWidth / 2)) / MOD_W);
if (index >= 0 && index < this.modules.length) {
const mod = this.modules[index];
if (mod.hp > 0) {
mod.hp -= damage;
if (mod.hp <= 0) {
mod.hp = 0;
this.crew.forEach(c => { if(c.pos === index && c.role !== 'captain') c.pos = -1; });
for(let i=0; i<15; i++) particles.push({x: hitX, y: hitY, vx: (Math.random()-0.5)*8, vy: (Math.random()-0.5)*8, life: 30, maxLife: 30, color: '#ffaa00', size: 3});
if (mod.type.includes('weapon')) {
if(index > 0) this.modules[index-1].hp -= 30;
if(index < this.modules.length-1) this.modules[index+1].hp -= 30;
for(let i=0; i<20; i++) particles.push({x: hitX, y: hitY, vx: (Math.random()-0.5)*15, vy: (Math.random()-0.5)*15, life: 40, maxLife: 40, color: '#ff5500', size: 4});
}
if (mod.type === 'core') {
this.dead = true;
for(let i=0; i<80; i++) particles.push({x: this.x, y: this.y, vx: (Math.random()-0.5)*25, vy: (Math.random()-0.5)*25, life: 80, maxLife: 80, color: Math.random()>0.5?'#fff':'#ff3300', size: Math.random()*6+2});
logMsg(`${this.name} 核心舱被摧毁!${this.captain} 阵亡。`);
if (selectedShip === this) updateTacticalUI();
}
}
}
}
}
tryIntercept() {
if (this.interceptCd > 0) { this.interceptCd--; return; }
const hasLaserPD = this.isModuleFunctional('weapon_laser');
const hasRailgunPD = this.isModuleFunctional('railgun');
const hasPd = this.isModuleFunctional('supply') || this.isModuleFunctional('missile');
if (!hasLaserPD && !hasRailgunPD && !hasPd) return;
let threat = null, minDist = 500;
for (let b of bullets) {
if (b.isEnemy !== this.isEnemy && b.type === 'missile') {
let d = dist(this, b); if (d < minDist) { minDist = d; threat = b; }
}
}
if (threat) {
this.interceptCd = 15;
const angle = angleTo(this, threat);
if (hasLaserPD) bullets.push(new Bullet(this.x, this.y, angle, this.isEnemy, 'laser_pd'));
else bullets.push(new Bullet(this.x, this.y, angle, this.isEnemy, 'pd_shell'));
}
}
updateCrew() {
this.crew.forEach(c => {
if (c.role === 'captain' || c.pos === -1) return;
if (c.targetIndex !== null) {
const mod = this.modules[c.targetIndex];
if (mod.hp >= 100 || mod.hp <= 0) { c.targetIndex = null; c.progress = 0; }
}
if (c.targetIndex === null) {
for(let i=0; i<this.modules.length; i++) {
if(this.modules[i].hp > 0 && this.modules[i].hp < 100) {
if(!this.crew.some(other => other.targetIndex === i)) { c.targetIndex = i; c.progress = 0; break; }
}
}
}
if (c.targetIndex !== null) {
if (c.pos !== c.targetIndex) {
c.progress += 20 * (1/60);
if (c.progress >= 20) { c.pos = c.targetIndex; c.progress = 0; }
} else {
this.modules[c.targetIndex].hp += 0.5 * (1/60);
if(this.modules[c.targetIndex].hp > 100) this.modules[c.targetIndex].hp = 100;
}
}
});
}
update() {
if (this.dead) return;
this.updateCrew();
this.tryIntercept();
if (this.x < 200 || this.x > WORLD_W - 200) this.vx *= -0.8;
if (this.y < 200 || this.y > WORLD_H - 200) this.vy *= -0.8;
this.vx *= 0.98; this.vy *= 0.98;
this.x += this.vx; this.y += this.vy;
const engineFunctional = this.isModuleFunctional('engine');
if (!engineFunctional) { this.vx *= 0.9; this.vy *= 0.9; return; }
this.logic();
}
aimTurret(target) {
if (!target) { this.turretAngle *= 0.95; return; }
const ta = angleTo(this, target);
const relativeAngle = normalizeAngle(ta - this.angle);
const diff = normalizeAngle(relativeAngle - this.turretAngle);
this.turretAngle += Math.sign(diff) * Math.min(Math.abs(diff), 0.08);
}
moveToTarget(targetAngle) {
const diff = normalizeAngle(targetAngle - this.angle);
this.angle += Math.sign(diff) * Math.min(Math.abs(diff), this.turnSpeed);
if (Math.abs(diff) < 0.3) {
this.vx += Math.cos(this.angle) * 0.06; this.vy += Math.sin(this.angle) * 0.06;
const speed = Math.hypot(this.vx, this.vy);
if (speed > this.maxSpeed) { this.vx = (this.vx / speed) * this.maxSpeed; this.vy = (this.vy / speed) * this.maxSpeed; }
}
}
dogfight(target, idealDist) {
const d = dist(this, target);
const ta = angleTo(this, target);
if (d > idealDist + 150) this.moveToTarget(ta);
else if (d < idealDist - 150) this.moveToTarget(ta + Math.PI);
else {
let orbitAngle = ta + (Math.PI / 3) * this.orbitDir;
this.moveToTarget(orbitAngle);
this.aimTurret(target);
}
}
draw() {
if (this.dead) return;
ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
if (this === selectedShip) {
ctx.strokeStyle = 'rgba(102, 204, 255, 0.6)'; ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.strokeRect(-this.totalWidth/2 - 10, -MOD_H/2 - 35, this.totalWidth + 20, MOD_H + 70);
ctx.setLineDash([]);
}
this.extras.forEach(ext => {
const module = this.modules[ext.index];
if(module) drawModule(ctx, { type: ext.type, hp: module.hp }, ext.index * MOD_W - this.totalWidth/2, -MOD_H/2, false);
});
this.modules.forEach((mod, i) => drawModule(ctx, mod, i * MOD_W - this.totalWidth/2, -MOD_H/2, i === this.modules.length - 1));
this.drawExtra(ctx);
ctx.restore();
}
drawExtra(ctx) {}
logic() {}
}
// ... (LaserShip, AssaultShip, MissileShip, SupplyShip 的定义与前版本完全一致,略) ...
class LaserShip extends Ship {
constructor(x, y, isEnemy) { super(x, y, isEnemy, '#66ccff', ['engine', 'supply', 'core', 'hab', 'weapon_laser'], [{ index: 1, type: 'solar' }, { index: 3, type: 'radiator' }]); this.maxSpeed = 2.5; }
logic() {
const weaponFunctional = this.isModuleFunctional('weapon_laser');
const mainGun = this.modules[4];
let attackMultiplier = this.isModuleFunctional('solar') ? 1.0 : 0.8;
let coolingRate = this.isModuleFunctional('radiator') ? 0.3 : 0.15;
if (mainGun && mainGun.hp > 0 && mainGun.isOverheated) { mainGun.heat -= coolingRate; if (mainGun.heat <= 0) mainGun.isOverheated = false; }
let target = null, minDist = 3500;
entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
this.target = target;
if (target) {
this.aimTurret(target); this.dogfight(target, 800);
if (weaponFunctional && !mainGun.isOverheated && Math.abs(normalizeAngle(angleTo(this, target) - this.angle)) < 0.35) {
bullets.push(new Bullet(this.x, this.y, this.angle, this.isEnemy, 'laser_main', this, attackMultiplier));
mainGun.heat = (mainGun.heat || 0) + 0.8; if (mainGun.heat >= 100) mainGun.isOverheated = true;
}
}
}
drawExtra(ctx) {
if (this.isModuleFunctional('weapon_laser') && this.target) {
drawTurret(ctx, -20, -MOD_H/2+2, this.turretAngle, 'laser_intercept'); drawTurret(ctx, -20, MOD_H/2-2, this.turretAngle, 'laser_intercept');
drawTurret(ctx, 5, -MOD_H/2+2, this.turretAngle, 'laser_intercept'); drawTurret(ctx, 5, MOD_H/2-2, this.turretAngle, 'laser_intercept');
}
}
}
class AssaultShip extends Ship {
constructor(x, y, isEnemy) { super(x, y, isEnemy, '#00ffcc', ['engine', 'supply', 'core', 'hab', 'missile', 'radar', 'railgun'], [{ index: 1, type: 'solar' }, { index: 2, type: 'solar' }, { index: 4, type: 'radiator' }]); this.railgunAmmo = 24; this.railgunCd = 0; this.maxSpeed = 4.5; }
logic() {
const weaponFunctional = this.isModuleFunctional('railgun');
const radarFunctional = this.isModuleFunctional('radar');
const detectRange = radarFunctional ? 5000 : 1500;
if (this.railgunCd > 0) this.railgunCd--;
let target = null, minDist = detectRange;
entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
this.target = target;
if (target) {
this.aimTurret(target); this.dogfight(target, 400);
if (weaponFunctional && this.railgunAmmo > 0 && this.railgunCd <= 0 && Math.abs(normalizeAngle(angleTo(this, target) - (this.angle + this.turretAngle))) < 0.15) {
bullets.push(new Bullet(this.x, this.y, this.angle + this.turretAngle, this.isEnemy, 'railgun'));
this.railgunAmmo--; this.railgunCd = 120;
}
}
}
drawExtra(ctx) { if (this.isModuleFunctional('railgun') && this.target) { drawTurret(ctx, 60, -MOD_H/2, this.turretAngle, 'railgun'); drawTurret(ctx, 60, MOD_H/2, this.turretAngle, 'railgun'); } }
}
class MissileShip extends Ship {
constructor(x, y, isEnemy) { super(x, y, isEnemy, '#ff3366', ['engine', 'hab', 'missile', 'solar', 'missile', 'missile', 'core', 'missile', 'missile', 'missile', 'supply', 'supply']); this.maxAmmo = 80; this.ammo = 80; this.cd = 0; this.maxSpeed = 2.0; }
logic() {
if (this.cd > 0) this.cd--;
if (this.ammo <= 0) { this.moveToTarget(this.angle + Math.PI); return; }
let target = null, minDist = 4500;
entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; target = e; } } });
this.target = target;
if (target) {
this.dogfight(target, 1200);
if (this.cd <= 0 && Math.abs(normalizeAngle(angleTo(this, target) - this.angle)) < 0.3) {
bullets.push(new Bullet(this.x, this.y, this.angle, this.isEnemy, 'missile', this)); this.ammo--; this.cd = 80;
}
}
}
drawExtra(ctx) { ctx.save(); ctx.rotate(-this.angle); ctx.fillStyle = '#fff'; ctx.font = '12px monospace'; ctx.textAlign = 'center'; ctx.fillText(`弹:${this.ammo}`, 0, 45); ctx.restore(); }
}
class SupplyShip extends Ship {
constructor(x, y, isEnemy) { super(x, y, isEnemy, '#ffaa00', ['engine', 'core', 'supply']); this.state = 'search'; this.friend = null; this.maxSpeed = 4.0; }
logic() {
if (this.state === 'search') {
if (this.isModuleFunctional('supply')) this.friend = entities.find(e => e.isEnemy === this.isEnemy && !e.dead && e.ammo !== undefined && e.ammo < 20);
else this.friend = null;
if (this.friend) {
this.moveToTarget(angleTo(this, this.friend));
if (dist(this, this.friend) < 50) this.state = 'docking';
} else this.moveToTarget(this.angle + (Math.random()-0.5)*0.1);
} else if (this.state === 'docking') {
this.vx = 0; this.vy = 0;
if (!this.friend || this.friend.dead || !this.isModuleFunctional('supply')) { this.state = 'search'; return; }
this.angle = angleTo(this, this.friend);
if (this.friend.ammo < this.friend.maxAmmo) { this.friend.ammo += 0.5; if (Math.random() < 0.1) particles.push({x: (this.x+this.friend.x)/2, y: (this.y+this.friend.y)/2, vx:0, vy:0, life: 15, maxLife:15, color: '#00ff00', size: 2}); }
else this.state = 'search';
}
}
}
// 子弹类与主循环逻辑保持不变
class Bullet {
constructor(x, y, angle, isEnemy, type, owner = null, multiplier = 1.0) {
this.x = x; this.y = y; this.isEnemy = isEnemy; this.type = type; this.angle = angle;
this.dead = false; this.life = 200; this.hp = 1;
if (type === 'missile') { this.speed = 7; this.damage = 25 * multiplier; this.turnSpeed = 0.08; this.life = 800; this.target = null; this.hp = 12; }
else if (type === 'railgun') { this.speed = 40; this.damage = 80 * multiplier; this.life = 200; }
else if (type === 'laser_main') { this.speed = 30; this.damage = 4 * multiplier; this.life = 20; }
else if (type === 'laser_pd') { this.speed = 35; this.damage = 6; this.life = 20; }
else if (type === 'pd_shell') { this.speed = 25; this.damage = 4; this.life = 30; }
if (type.includes('laser') && owner) { this.vx = owner.vx + Math.cos(angle)*this.speed; this.vy = owner.vy + Math.sin(angle)*this.speed; }
else { this.vx = Math.cos(angle)*this.speed; this.vy = Math.sin(angle)*this.speed; }
}
update() {
if (this.dead) return;
if (this.type === 'missile') {
if (!this.target || this.target.dead) { let minDist = Infinity; entities.forEach(e => { if(e.isEnemy !== this.isEnemy && !e.dead) { let d = dist(this, e); if(d < minDist) { minDist = d; this.target = e; } } }); }
if (this.target) { const ta = angleTo(this, this.target); const currentAngle = Math.atan2(this.vy, this.vx); const diff = normalizeAngle(ta - currentAngle); const newAngle = currentAngle + Math.sign(diff) * this.turnSpeed; this.vx = Math.cos(newAngle) * this.speed; this.vy = Math.sin(newAngle) * this.speed; this.angle = newAngle; }
}
this.x += this.vx; this.y += this.vy; this.life--;
if (this.life <= 0 || this.x < 0 || this.x > WORLD_W || this.y < 0 || this.y > WORLD_H) this.dead = true;
for (let e of entities) {
if (!e.dead && e.isEnemy !== this.isEnemy) {
if (this.type === 'laser_pd' || this.type === 'pd_shell') {
for (let b of bullets) {
if (!b.dead && b.isEnemy !== this.isEnemy && b.type === 'missile') {
if (dist(this, b) < 30) { b.hp -= this.damage; this.dead = true; if (b.hp <= 0) { b.dead = true; for(let i=0; i<8; i++) particles.push({x:b.x, y:b.y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6, life:15, maxLife:15, color:'#fff', size:2}); } return; }
}
}
} else if (this.type !== 'laser_pd' && this.type !== 'pd_shell') {
if (dist(this, e) < e.radius) { e.takeDamage(this.x, this.y, this.damage); this.dead = true; }
}
}
}
}
draw() {
if (this.dead) return;
ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.angle);
if (this.type === 'missile') { ctx.fillStyle = '#ffaa00'; ctx.fillRect(-10, -3, 20, 6); ctx.fillStyle = 'rgba(255,50,0,0.8)'; ctx.beginPath(); ctx.moveTo(-10, -3); ctx.lineTo(-22, 0); ctx.lineTo(-10, 3); ctx.fill(); }
else if (this.type === 'railgun') { ctx.fillStyle = '#00ffcc'; ctx.fillRect(-25, -3, 50, 6); }
else if (this.type === 'laser_main') { ctx.fillStyle = 'rgba(102, 204, 255, 0.9)'; ctx.fillRect(-30, -2, 60, 4); }
else if (this.type === 'laser_pd') { ctx.fillStyle = '#fff'; ctx.fillRect(-15, -1.5, 30, 3); }
else if (this.type === 'pd_shell') { ctx.fillStyle = '#ffaa00'; ctx.fillRect(-4, -1.5, 8, 3); }
ctx.restore();
}
}
function startBattle() {
document.getElementById('ui-layer').style.opacity = '0';
setTimeout(() => { document.getElementById('ui-layer').style.display = 'none'; }, 300);
gameState = 'playing'; entities = []; bullets = []; particles = []; selectedShip = null; uiEl.style.display = 'none';
const genShips = (prefix, isEnemy, baseX) => {
let yOffset = -500;
for(let i=0; i<config[prefix+'Laser']; i++) { entities.push(new LaserShip(baseX, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
for(let i=0; i<config[prefix+'Assault']; i++) { entities.push(new AssaultShip(baseX + 300, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
for(let i=0; i<config[prefix+'Missile']; i++) { entities.push(new MissileShip(baseX, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
for(let i=0; i<config[prefix+'Supply']; i++) { entities.push(new SupplyShip(baseX + 300, WORLD_H/2 + yOffset, isEnemy)); yOffset += 300; }
};
genShips('b', false, 1000); genShips('r', true, WORLD_W - 1000);
cam.x = WORLD_W/2; cam.y = WORLD_H/2; cam.zoom = 0.15; loop();
}
function loop() {
if (gameState !== 'playing') return;
if (selectedShip && !selectedShip.dead) {
if (!isDragging) {
cam.x += (selectedShip.x - cam.x) * 0.05;
cam.y += (selectedShip.y - cam.y) * 0.05;
}
updateTacticalUI();
} else if (selectedShip && selectedShip.dead) {
selectedShip = null; uiEl.style.display = 'none';
}
ctx.fillStyle = '#03050a'; ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.translate(W / 2, H / 2); ctx.scale(cam.zoom, cam.zoom); ctx.translate(-cam.x, -cam.y);
ctx.strokeStyle = 'rgba(50, 100, 150, 0.1)';
for(let i=0; i<WORLD_W; i+=400) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, WORLD_H); ctx.stroke(); }
for(let i=0; i<WORLD_H; i+=400) { ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(WORLD_W, i); ctx.stroke(); }
for (let e of entities) { e.update(); if (!e.dead) e.draw(); }
for (let i = bullets.length - 1; i >= 0; i--) { bullets[i].update(); if (!bullets[i].dead) bullets[i].draw(); else bullets.splice(i, 1); }
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i]; p.x += p.vx; p.y += p.vy; p.life--;
ctx.globalAlpha = Math.max(0, p.life / p.maxLife); ctx.fillStyle = p.color;
ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI*2); ctx.fill();
if (p.life <= 0) particles.splice(i, 1);
}
ctx.globalAlpha = 1;
ctx.restore();
if (!entities.some(e => !e.isEnemy && !e.dead) || !entities.some(e => e.isEnemy && !e.dead)) {
gameState = 'over';
ctx.fillStyle = 'rgba(0,0,0,0.8)'; ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff'; ctx.font = 'bold 30px monospace'; ctx.textAlign = 'center';
ctx.fillText('舰队交战结束', W/2, H/2); return;
}
requestAnimationFrame(loop);
}
</script>
</body>
</html>Game Source: 深空舰队 - 极简战术版
Creator: CosmicRider48
Libraries: none
Complexity: complex (676 lines, 34.4 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-cosmicrider48-mvnkv8dx" to link back to the original. Then publish at arcadelab.ai/publish.