2036 舰队交战 - 完全体
by ApexFalcon66697 lines33.5 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>2036 舰队交战 - 完全体</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background: #04060f; font-family: system-ui, 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: 4000px; height: 4000px; margin-left: -2000px; margin-top: -2000px; transform-origin: 50% 50%; will-change: transform; background: radial-gradient(circle at 50% 50%, #0a1225 0%, #03050c 100%); }
/* 星空背景容器 */
#starLayer { position: absolute; width: 4000px; height: 4000px; top: 0; left: 0; pointer-events: none; }
.star { position:absolute; background:#fff; border-radius:50%; animation: twinkle 3s infinite ease-in-out; }
@keyframes twinkle { 0%,100% { opacity:.15; } 50% { opacity:.95; } }
/* ================= 舰船与模块 ================= */
.ship { position: absolute; left: 0; top: 0; width: 0; height: 0; will-change: transform; z-index: 10; transform-origin: 0 0; }
/* 舱段基础样式 */
.mod { position: absolute; border-radius: 2px; box-shadow: 0 2px 8px rgba(0,0,0,0.5); }
.theme-blue { --c1:#fff; --c2:#dce4ee; --c3:#b8c6d6; --bd:#7a8ea2; --p1:#1d4d86; --p2:#2b6cb5; --accent:#4a90d9; }
.theme-red { --c1:#fff4ef; --c2:#f0dcd4; --c3:#c9b3a8; --bd:#8a5a4a; --p1:#8a2a1d; --p2:#b84530; --accent:#e05a3a; }
/* 各类型模块 */
.mod-body { background: linear-gradient(180deg, var(--c1) 0%, var(--c2) 50%, var(--c3) 100%); border: 1px solid var(--bd); z-index: 10; }
.mod-panel { background: repeating-linear-gradient(90deg, var(--p1) 0px, var(--p1) 8px, var(--p2) 8px, var(--p2) 10px); border: 1px solid var(--accent); box-shadow: 0 0 10px var(--accent); z-index: 5; }
.mod-heat { background: repeating-linear-gradient(0deg, #8a2a1d 0px, #8a2a1d 4px, #e05a3a 4px, #e05a3a 6px); border: 1px solid #e05a3a; z-index: 5; }
.mod-radar { background: linear-gradient(180deg, #3a4b5c, #1c2631); border: 1px solid #6a8aaa; box-shadow: inset 0 0 10px rgba(80,180,255,0.5); z-index: 11; }
.mod-missile { background: repeating-linear-gradient(90deg, #2d3748 0px, #2d3748 6px, #4a5568 6px, #4a5568 8px); border: 1px solid #718096; z-index: 12; }
.mod-port { background: #1a2a3a; border: 2px solid #4ade80; box-shadow: 0 0 10px rgba(74,222,128,0.5); z-index: 12; }
/* 桁架 */
.truss { position: absolute; background: #475569; border: 1px solid #1e293b; z-index: 2; }
/* ================= 炮塔与武器 ================= */
.turret { position: absolute; width: 0; height: 0; z-index: 20; }
.turret-base { position: absolute; width: 16px; height: 16px; margin: -8px 0 0 -8px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #fff, #9aaec2 70%, #5f7fa0); border: 1px solid #4a5a6a; box-shadow: 0 2px 4px rgba(0,0,0,0.5); }
.turret-barrel { position: absolute; left: 0; top: 50%; margin-top: -3px; height: 6px; border-radius: 0 2px 2px 0; transform-origin: 0 50%; }
.barrel-rail { width: 35px; background: linear-gradient(180deg, #fff, #7fdcff, #2b6cb5); box-shadow: 0 0 8px #7fdcff; border: 1px solid #aee; }
.barrel-ciws { width: 16px; background: linear-gradient(180deg, #fff, #a0aec0, #4a5568); border: 1px solid #ccc; }
.barrel-laser { width: 45px; background: linear-gradient(180deg, #ffaaaa, #ff2222, #880000); box-shadow: 0 0 12px #ff2222; border: 1px solid #f88; }
/* ================= 血条与特效 ================= */
.hp-bar { position:absolute; height:4px; background:rgba(0,0,0,0.7); border:1px solid #555; border-radius:2px; opacity:0; z-index: 30; transition: opacity 0.3s; pointer-events: none; }
.hp-fill { height:100%; background:#4ade80; width:100%; transition: width 0.1s, background 0.3s; }
.hp-bar.show { opacity: 1; }
.projectile { position:absolute; left:0; top:0; pointer-events:none; z-index: 15; will-change: transform; }
.p-rail { width:24px; height:4px; margin:-2px 0 0 -12px; background:#7fdcff; box-shadow:0 0 10px #7fdcff; border-radius:2px; }
.p-missile { width:12px; height:6px; margin:-3px 0 0 -6px; background:#e8eef5; border-radius:0 3px 3px 0; box-shadow:0 0 5px #f90; }
.p-missile::before { content:""; position:absolute; right:100%; width:15px; height:6px; background:linear-gradient(90deg, transparent, #f90); animation: flame 0.1s infinite alternate; }
.p-ciws { width:4px; height:4px; margin:-2px 0 0 -2px; background:#ff0; border-radius:50%; box-shadow:0 0 6px #ff0; }
@keyframes flame { from { opacity: 0.6; } to { opacity: 1; } }
.laser-beam { position:absolute; height:4px; transform-origin:0 50%; background:linear-gradient(90deg, #fff, #f00); box-shadow:0 0 15px #f00; z-index:15; border-radius:2px; }
.supply-beam { position:absolute; height:4px; transform-origin:0 50%; background:linear-gradient(90deg, #4ade80, transparent); box-shadow:0 0 10px #4ade80; z-index:15; border-radius:2px; animation:pulse 0.3s infinite alternate; }
@keyframes pulse { from { opacity: 0.5; } to { opacity: 1; } }
.muzzle { position:absolute; width:30px; height:30px; margin:-15px 0 0 -15px; border-radius:50%; background:radial-gradient(circle, #fff, transparent); animation:pop 0.15s forwards; pointer-events:none; z-index:16; }
@keyframes pop { from { transform:scale(0.5); opacity:1; } to { transform:scale(1.5); opacity:0; } }
.frag { position:absolute; border-radius:1px; animation:fly 0.8s ease-out forwards; z-index:18; }
@keyframes fly { 0% { transform:translate(0,0) rotate(0); opacity:1; } 100% { transform:translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity:0; } }
/* ================= 推进器喷火 ================= */
.thruster-flame { position:absolute; width:0; height:0; transform-origin:0 0; pointer-events:none; z-index: 1; opacity:0; will-change: transform, opacity; }
.tf-glow { position:absolute; left:-20px; top:-25px; width:80px; height:50px; background:radial-gradient(ellipse at 20% 50%, rgba(255,150,50,0.6) 0%, transparent 70%); border-radius:50%; }
.tf-core { position:absolute; left:0px; top:-6px; width:40px; height:12px; background:radial-gradient(ellipse at 0% 50%, #fff, #ffd166, transparent); clip-path: polygon(0 50%, 100% 10%, 100% 90%); animation: flame 0.1s infinite alternate; }
/* ================= UI 面板 ================= */
.overlay { position:fixed; inset:0; z-index:100; display:flex; align-items:center; justify-content:center; background:rgba(4,8,18,0.85); backdrop-filter:blur(5px); transition:opacity 0.4s; }
.overlay.hide { opacity:0; pointer-events:none; }
.panel { width:min(95vw, 500px); padding:20px; background:linear-gradient(180deg, #182846, #0c1426); border:1px solid #4af; border-radius:12px; box-shadow:0 10px 40px rgba(0,100,255,0.2); text-align:center; color:#cfe6ff; }
.panel h1 { font-size:18px; letter-spacing:2px; margin-bottom:20px; color:#fff; text-shadow:0 0 10px #4af; }
.fleet-setup { display:flex; gap:10px; margin-bottom:20px; }
.fleet-col { flex:1; background:rgba(0,0,0,0.3); padding:10px; border-radius:8px; border:1px solid #444; }
.fleet-col.blue { border-color:#4af; }
.fleet-col.red { border-color:#f64; }
.ship-row { display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; font-size:13px; }
.btns button { width:24px; height:24px; background:#4af; color:#000; border:none; border-radius:4px; font-weight:bold; cursor:pointer; }
.btns button:active { transform:scale(0.9); }
.start-btn { width:100%; padding:12px; font-size:16px; font-weight:bold; letter-spacing:2px; background:#4af; color:#000; border:none; border-radius:8px; cursor:pointer; box-shadow:0 4px 15px rgba(0,100,255,0.4); }
.hud { position:fixed; top:10px; left:10px; right:10px; display:flex; justify-content:space-between; pointer-events:none; z-index:50; color:#fff; font-size:12px; text-shadow:1px 1px 2px #000; }
.hud-bar { width:120px; height:6px; background:#333; border-radius:3px; overflow:hidden; margin-top:4px; border: 1px solid #555; }
.hud-fill { height:100%; transition:width 0.2s; }
.hud-fill.blue { background:#4af; }
.hud-fill.red { background:#f64; }
</style>
</head>
<body>
<div class="viewport" id="viewport">
<div class="scene" id="scene">
<div id="starLayer"></div>
</div>
</div>
<div class="hud">
<div>
<div>蓝方编队</div>
<div class="hud-bar"><div class="hud-fill blue" id="hudA" style="width:100%"></div></div>
</div>
<div style="text-align:right">
<div>红方编队</div>
<div class="hud-bar" style="margin-left:auto"><div class="hud-fill red" id="hudB" style="width:100%"></div></div>
</div>
</div>
<div class="overlay" id="overlay">
<div class="panel">
<h1>2036 · 舰队部署</h1>
<div class="fleet-setup">
<div class="fleet-col blue">
<div style="color:#4af; font-weight:bold; margin-bottom:10px;">蓝方</div>
<div class="ship-row"><span>突击舰</span> <div class="btns"><button data-side="A" data-type="assault" data-d="-1">-</button> <span id="cntA_assault">1</span> <button data-side="A" data-type="assault" data-d="1">+</button></div></div>
<div class="ship-row"><span>激光舰</span> <div class="btns"><button data-side="A" data-type="laser" data-d="-1">-</button> <span id="cntA_laser">1</span> <button data-side="A" data-type="laser" data-d="1">+</button></div></div>
<div class="ship-row"><span>补给舰</span> <div class="btns"><button data-side="A" data-type="supply" data-d="-1">-</button> <span id="cntA_supply">1</span> <button data-side="A" data-type="supply" data-d="1">+</button></div></div>
</div>
<div class="fleet-col red">
<div style="color:#f64; font-weight:bold; margin-bottom:10px;">红方</div>
<div class="ship-row"><span>突击舰</span> <div class="btns"><button data-side="B" data-type="assault" data-d="-1">-</button> <span id="cntB_assault">1</span> <button data-side="B" data-type="assault" data-d="1">+</button></div></div>
<div class="ship-row"><span>激光舰</span> <div class="btns"><button data-side="B" data-type="laser" data-d="-1">-</button> <span id="cntB_laser">1</span> <button data-side="B" data-type="laser" data-d="1">+</button></div></div>
<div class="ship-row"><span>补给舰</span> <div class="btns"><button data-side="B" data-type="supply" data-d="-1">-</button> <span id="cntB_supply">1</span> <button data-side="B" data-type="supply" data-d="1">+</button></div></div>
</div>
</div>
<button class="start-btn" id="startBtn">开始交战</button>
</div>
</div>
<script>
(function(){
'use strict';
const state = { ships: [], projectiles: [], started: false, over: false };
// 初始视角变量
let scale = 0.15, tx = 0, ty = 0;
const viewport = document.getElementById('viewport');
const scene = document.getElementById('scene');
function getDist(ax, ay, bx, by) { return Math.hypot(bx - ax, by - ay); }
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function pointInRotatedRect(px, py, cx, cy, w, h, angle) {
const cos = Math.cos(-angle);
const sin = Math.sin(-angle);
const dx = px - cx;
const dy = py - cy;
const lx = dx * cos - dy * sin;
const ly = dx * sin + dy * cos;
return Math.abs(lx) <= w/2 && Math.abs(ly) <= h/2;
}
// 星空
(function initStars() {
const layer = document.getElementById('starLayer');
let html = '';
for (let i=0; i<500; i++) {
let s = Math.random()*2 + 1;
html += `<div class="star" style="width:${s}px;height:${s}px;left:${Math.random()*100}%;top:${Math.random()*100}%;animation-delay:${Math.random()*3}s"></div>`;
}
layer.innerHTML = html;
})();
/* ================= 战舰数据定义 ================= */
const SHIP_DEFS = {
assault: {
name: '突击舰', maxSpeed: 400, turnSpeed: 3, radar: 4000,
modules: [
{ id:'core', x:-25, y:-25, w:50, h:50, type:'body', hp:300 },
{ id:'weapon', x:25, y:-20, w:40, h:40, type:'body', hp:150 },
{ id:'life', x:-75, y:-20, w:25, h:40, type:'body', hp:100 },
{ id:'radar', x:-50, y:-15, w:25, h:30, type:'radar', hp:100 },
{ id:'engine', x:-105, y:-12, w:30, h:24, type:'body', hp:120 },
{ id:'missile1', x:30, y:-35, w:30, h:15, type:'missile', hp:80 },
{ id:'missile2', x:30, y:20, w:30, h:15, type:'missile', hp:80 },
{ id:'supply', x:-20, y:25, w:40, h:15, type:'body', hp:80 }
],
trusses: [{x:-40, y:-25, w:6, h:20}, {x:30, y:-25, w:6, h:20}, {x:-40, y:5, w:6, h:20}, {x:30, y:5, w:6, h:20}],
panels: [{x:-70, y:-45, w:60, h:20}, {x:0, y:-45, w:60, h:20}, {x:-70, y:25, w:60, h:20}, {x:0, y:25, w:60, h:20}],
heats: [{x:-45, y:-20, w:20, h:40}, {x:25, y:-20, w:20, h:40}],
turrets: [{ type:'rail', x:45, y:-10, w:35, h:6 }, { type:'rail', x:45, y:10, w:35, h:6 }, { type:'ciws', x:20, y:0, w:16, h:6 }],
weaponData: { rail: { max: 24, reload: 2.0 }, missile: { max: 2, reload: 5.0 }, ciws: { max: 15000 } }
},
laser: {
name: '激光舰', maxSpeed: 200, turnSpeed: 2, radar: 2000,
modules: [
{ id:'weapon', x:15, y:-25, w:45, h:50, type:'body', hp:150 },
{ id:'core', x:-25, y:-30, w:40, h:60, type:'body', hp:300 },
{ id:'life', x:-55, y:-20, w:30, h:40, type:'body', hp:100 },
{ id:'engine', x:-85, y:-15, w:30, h:30, type:'body', hp:120 },
{ id:'supply', x:-20, y:30, w:30, h:15, type:'body', hp:80 }
],
trusses: [{x:-20, y:-25, w:6, h:20}, {x:10, y:-25, w:6, h:20}, {x:-20, y:5, w:6, h:20}, {x:10, y:5, w:6, h:20}],
panels: [{x:-50, y:-45, w:60, h:20}, {x:-20, y:25, w:60, h:20}],
heats: [{x:-30, y:-20, w:20, h:15}, {x:0, y:-20, w:20, h:15}],
turrets: [{ type:'laser', x:60, y:0, w:50, h:10 }, { type:'ciws', x:30, y:-25, w:16, h:6 }, { type:'ciws', x:30, y:25, w:16, h:6 }, { type:'ciws', x:50, y:-20, w:16, h:6 }, { type:'ciws', x:50, y:20, w:16, h:6 }],
weaponData: { laser: { maxHeat: 15, coolTime: 8 }, ciws: { max: 15000 } }
},
supply: {
name: '补给舰', maxSpeed: 400, turnSpeed: 4, radar: 2000,
modules: [
{ id:'core', x:-30, y:-30, w:70, h:60, type:'body', hp:500 },
{ id:'engine', x:-60, y:-15, w:30, h:30, type:'body', hp:150 },
{ id:'life', x:-75, y:-10, w:15, h:20, type:'body', hp:200 },
{ id:'port1', x:40, y:-15, w:5, h:30, type:'port', hp:50 },
{ id:'port2', x:-5, y:-35, w:30, h:5, type:'port', hp:50 },
{ id:'port3', x:-5, y:30, w:30, h:5, type:'port', hp:50 }
],
trusses: [{x:-30, y:-35, w:6, h:20}, {x:20, y:-35, w:6, h:20}, {x:-30, y:15, w:6, h:20}, {x:20, y:15, w:6, h:20}],
panels: [{x:-60, y:-55, w:60, h:20}, {x:-10, y:-55, w:60, h:20}, {x:-60, y:35, w:60, h:20}, {x:-10, y:35, w:60, h:20}],
heats: [],
turrets: [ { type:'ciws', x:0, y:0, w:16, h:6 } ],
weaponData: { ciws: { max: 15000 } }
}
};
/* ================= 舰船类 ================= */
class Ship {
constructor(team, type, x, y) {
this.team = team; this.type = type; this.def = SHIP_DEFS[type];
this.x = x; this.y = y; this.angle = team === 'A' ? 0 : Math.PI;
this.vx = 0; this.vy = 0; this.alive = true;
this.radius = 60;
this.radarRange = this.def.radar;
this.ammo = { rail: 0, missile: 0, ciws: 0, laserHeat: 0, laserCool: 0 };
if (type === 'assault') { this.ammo.rail = 24; this.ammo.missile = 2; this.ammo.ciws = 15000; }
if (type === 'laser') { this.ammo.ciws = 15000; }
if (type === 'supply') { this.ammo.ciws = 15000; }
this.cooldowns = { rail: 0, missile: 0, ciws: 0 };
this.enemy = null;
this.supplyTarget = null;
this.prefDist = 300 + Math.random() * 200;
this.orbitDir = Math.random() < 0.5 ? -1 : 1;
this.strafeFreq = 0.5 + Math.random() * 1.0;
this.phase = Math.random() * Math.PI * 2;
this.wanderTimer = 1 + Math.random() * 2;
this.el = document.createElement('div');
this.el.className = `ship theme-${team === 'A' ? 'blue' : 'red'}`;
this.buildDOM();
scene.appendChild(this.el);
this.updateDOM();
}
buildDOM() {
this.modules = [];
this.def.modules.forEach(m => {
const div = document.createElement('div');
div.className = `mod mod-${m.type}`;
div.style.left = m.x + 'px'; div.style.top = m.y + 'px';
div.style.width = m.w + 'px'; div.style.height = m.h + 'px';
const bar = document.createElement('div');
bar.className = 'hp-bar';
bar.style.width = m.w + 'px'; bar.style.left = '0px'; bar.style.top = '-6px';
bar.innerHTML = '<div class="hp-fill"></div>';
div.appendChild(bar);
this.el.appendChild(div);
this.modules.push({ def: m, el: div, bar: bar, fill: bar.firstChild, hp: m.hp, maxHp: m.hp, alive: true, hideTimer: null });
});
if (this.def.trusses) this.def.trusses.forEach(t => {
const div = document.createElement('div');
div.className = 'truss';
div.style.left = t.x + 'px'; div.style.top = t.y + 'px';
div.style.width = t.w + 'px'; div.style.height = t.h + 'px';
this.el.appendChild(div);
});
if (this.def.panels) this.def.panels.forEach(p => {
const div = document.createElement('div');
div.className = 'mod mod-panel';
div.style.left = p.x + 'px'; div.style.top = p.y + 'px';
div.style.width = p.w + 'px'; div.style.height = p.h + 'px';
this.el.appendChild(div);
});
if (this.def.heats) this.def.heats.forEach(h => {
const div = document.createElement('div');
div.className = 'mod mod-heat';
div.style.left = h.x + 'px'; div.style.top = h.y + 'px';
div.style.width = h.w + 'px'; div.style.height = h.h + 'px';
this.el.appendChild(div);
});
const engineMod = this.def.modules.find(m => m.id === 'engine');
if (engineMod) {
this.flameEl = document.createElement('div');
this.flameEl.className = 'thruster-flame';
this.flameEl.innerHTML = '<div class="tf-glow"></div><div class="tf-core"></div>';
this.flameEl.style.left = engineMod.x + 'px';
this.flameEl.style.top = (engineMod.y + engineMod.h/2) + 'px';
this.el.appendChild(this.flameEl);
}
this.turrets = [];
this.def.turrets.forEach(t => {
const wrap = document.createElement('div');
wrap.className = 'turret';
wrap.style.left = t.x + 'px'; wrap.style.top = t.y + 'px';
const base = document.createElement('div');
base.className = 'turret-base';
if (t.type === 'laser') { base.style.width = '24px'; base.style.height = '24px'; base.style.margin = '-12px 0 0 -12px'; }
if (t.type === 'ciws' && this.type === 'laser') { base.style.background = 'radial-gradient(circle, #f88, #a22)'; }
wrap.appendChild(base);
const barrel = document.createElement('div');
barrel.className = `turret-barrel barrel-${t.type === 'laser' ? 'laser' : (t.type === 'rail' ? 'rail' : 'ciws')}`;
barrel.style.width = t.w + 'px'; barrel.style.height = t.h + 'px';
barrel.style.marginTop = (-t.h/2) + 'px';
wrap.appendChild(barrel);
this.el.appendChild(wrap);
this.turrets.push({ type: t.type, el: wrap, barrel: barrel });
});
}
updateDOM() {
this.el.style.transform = `translate3d(${this.x}px, ${this.y}px, 0) rotate(${this.angle}rad)`;
}
takeDamage(mod, dmg, px, py) {
if (!mod.alive || !this.alive) return;
mod.hp -= dmg;
mod.bar.classList.add('show');
mod.fill.style.width = Math.max(0, (mod.hp / mod.maxHp) * 100) + '%';
mod.fill.style.background = mod.hp > mod.maxHp*0.5 ? '#4ade80' : (mod.hp > mod.maxHp*0.2 ? '#facc15' : '#f87171');
clearTimeout(mod.hideTimer);
mod.hideTimer = setTimeout(() => { if (mod.alive) mod.bar.classList.remove('show'); }, 2000);
if (mod.hp <= 0) {
mod.alive = false;
mod.el.style.display = 'none';
for(let i=0; i<10; i++) spawnFrag(px||this.x, py||this.y, this.team);
if (mod.def.id === 'core') this.die();
}
}
die() {
this.alive = false;
this.el.style.display = 'none';
for(let i=0; i<30; i++) spawnFrag(this.x, this.y, this.team);
checkGameOver();
}
updateRadar() {
if (this.type === 'supply') {
let bestF = null, minD = Infinity;
state.ships.forEach(s => {
if (s !== this && s.team === this.team && s.alive && s.type === 'assault') {
if (s.ammo.rail < 5 || s.ammo.missile < 1) {
let d = getDist(this.x, this.y, s.x, s.y);
if (d < this.radarRange && d < minD) { minD = d; bestF = s; }
}
}
});
if (bestF) { this.supplyTarget = bestF; return; } else this.supplyTarget = null;
}
if (!this.enemy || !this.enemy.alive) {
let bestE = null, minD = Infinity;
state.ships.forEach(s => {
if (s.team !== this.team && s.alive) {
let d = getDist(this.x, this.y, s.x, s.y);
if (d < this.radarRange && d < minD) { minD = d; bestE = s; }
}
});
this.enemy = bestE;
}
}
update(dt) {
if (!this.alive) return;
this.cooldowns.rail = Math.max(0, this.cooldowns.rail - dt);
this.cooldowns.missile = Math.max(0, this.cooldowns.missile - dt);
this.cooldowns.ciws = Math.max(0, this.cooldowns.ciws - dt);
if (this.type === 'laser' && this.ammo.laserCool > 0) {
this.ammo.laserCool -= dt; if (this.ammo.laserCool <= 0) this.ammo.laserHeat = 0;
}
this.updateRadar();
let moveX = this.x, moveY = this.y;
let thrustMag = 0;
if (this.type === 'supply' && this.supplyTarget) {
const target = this.supplyTarget;
const dist = getDist(this.x, this.y, target.x, target.y);
if (dist > 60) { moveX = target.x; moveY = target.y; thrustMag = 0.8; }
else {
this.vx *= 0.8; this.vy *= 0.8;
this.angle = Math.atan2(target.y - this.y, target.x - this.x);
if (target.ammo.rail < 24) target.ammo.rail = Math.min(24, target.ammo.rail + 2*dt);
if (target.ammo.missile < 2) target.ammo.missile = Math.min(2, target.ammo.missile + 0.5*dt);
if (Math.random() < 0.3) spawnSupplyBeam(this.x, this.y, target.x, target.y);
thrustMag = 0;
}
} else if (this.enemy && this.enemy.alive) {
const dist = getDist(this.x, this.y, this.enemy.x, this.enemy.y);
const angToE = Math.atan2(this.enemy.y - this.y, this.enemy.x - this.x);
this.wanderTimer -= dt;
if (this.wanderTimer <= 0) {
this.wanderTimer = 1.5 + Math.random() * 2;
this.prefDist = 300 + Math.random() * 200;
this.orbitDir = Math.random() < 0.5 ? -1 : 1;
}
let wantD = this.type === 'laser' ? 600 : this.prefDist;
let targetAngle = angToE;
if (dist > wantD + 50) { targetAngle = angToE; thrustMag = 0.8; }
else if (dist < wantD - 50) { targetAngle = angToE + Math.PI; thrustMag = 0.8; }
else { targetAngle = angToE + Math.PI/2 * this.orbitDir; thrustMag = 0.5; }
let diff = targetAngle - this.angle;
while(diff > Math.PI) diff -= Math.PI*2;
while(diff < -Math.PI) diff += Math.PI*2;
this.angle += clamp(diff, -this.def.turnSpeed * dt, this.def.turnSpeed * dt);
this.vx = Math.cos(this.angle) * this.def.maxSpeed * thrustMag;
this.vy = Math.sin(this.angle) * this.def.maxSpeed * thrustMag;
this.x += this.vx * dt; this.y += this.vy * dt;
} else {
this.vx *= 0.9; this.vy *= 0.9;
this.x += this.vx * dt; this.y += this.vy * dt;
}
this.x = clamp(this.x, 500, 3500);
this.y = clamp(this.y, 500, 3500);
this.updateDOM();
if (this.flameEl) {
if (thrustMag > 0.1) {
this.flameEl.style.opacity = '1';
this.flameEl.style.transform = `rotate(${Math.PI}rad) scaleX(${0.6 + thrustMag})`;
} else {
this.flameEl.style.opacity = '0';
}
}
this.handleWeapons(dt);
}
handleWeapons(dt) {
const enemy = this.enemy;
if (!enemy || !enemy.alive) return;
const targetAng = Math.atan2(enemy.y - this.y, enemy.x - this.x);
const localAng = targetAng - this.angle;
const dist = getDist(this.x, this.y, enemy.x, enemy.y);
this.turrets.forEach(t => {
if (t.type === 'laser' || t.type === 'rail' || t.type === 'ciws') t.barrel.style.transform = `rotate(${localAng}rad)`;
});
if (this.ammo.ciws > 0 && this.cooldowns.ciws <= 0) {
for (let p of state.projectiles) {
if (p.team !== this.team && p.type !== 'ciws' && getDist(this.x, this.y, p.x, p.y) < 500) {
this.ammo.ciws--; this.cooldowns.ciws = 0.05;
const a = Math.atan2(p.y - this.y, p.x - this.x) + (Math.random()-0.5)*0.2;
spawnProjectile('ciws', this.x, this.y, a, 2500, 20, this.team);
break;
}
}
}
if (this.type === 'assault') {
if (dist < 1200 && Math.abs(localAng) < 0.4) {
if (this.ammo.rail > 0 && this.cooldowns.rail <= 0) {
this.ammo.rail--; this.cooldowns.rail = this.def.weaponData.rail.reload;
spawnProjectile('rail', this.x, this.y, targetAng, 2200, 40, this.team);
spawnMuzzle(this.x, this.y);
}
if (this.ammo.missile > 0 && this.cooldowns.missile <= 0) {
this.ammo.missile--; this.cooldowns.missile = this.def.weaponData.missile.reload;
spawnProjectile('missile', this.x, this.y, targetAng, 600, 80, this.team);
}
}
} else if (this.type === 'laser') {
if (dist < 900 && Math.abs(localAng) < 0.1 && this.ammo.laserHeat < this.def.weaponData.laser.maxHeat && this.ammo.laserCool <= 0) {
this.ammo.laserHeat += dt;
enemy.modules.forEach(m => { if(m.alive) enemy.takeDamage(m, 20*dt, m.x+enemy.x, m.y+enemy.y); });
if (Math.random() < 0.4) spawnLaser(this.x, this.y, targetAng, dist);
if (this.ammo.laserHeat >= this.def.weaponData.laser.maxHeat) this.ammo.laserCool = this.def.weaponData.laser.coolTime;
}
}
}
}
/* ================= 投影物与特效 ================= */
function spawnProjectile(type, x, y, angle, speed, dmg, team) {
const el = document.createElement('div');
el.className = `projectile p-${type}`;
scene.appendChild(el);
state.projectiles.push({ el, type, team, x, y, dmg, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 0, maxLife: 2.0, angle });
}
function spawnLaser(x, y, ang, len) {
const el = document.createElement('div');
el.className = 'laser-beam';
el.style.width = len + 'px';
el.style.transform = `translate3d(${x}px, ${y}px, 0) rotate(${ang}rad)`;
scene.appendChild(el);
setTimeout(() => el.remove(), 60);
}
function spawnSupplyBeam(x1, y1, x2, y2) {
const ang = Math.atan2(y2-y1, x2-x1);
const len = getDist(x1,y1,x2,y2);
const el = document.createElement('div');
el.className = 'supply-beam';
el.style.width = len + 'px';
el.style.transform = `translate3d(${x1}px, ${y1}px, 0) rotate(${ang}rad)`;
scene.appendChild(el);
setTimeout(() => el.remove(), 300);
}
function spawnMuzzle(x, y) {
const el = document.createElement('div');
el.className = 'muzzle';
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
scene.appendChild(el);
setTimeout(() => el.remove(), 200);
}
function spawnFrag(x, y, team) {
const el = document.createElement('div');
el.className = 'frag';
const s = 3 + Math.random()*5;
el.style.width = s+'px'; el.style.height = s+'px';
el.style.background = team === 'A' ? '#7fdcff' : '#ffa060';
el.style.left = x+'px'; el.style.top = y+'px';
const a = Math.random()*Math.PI*2;
const d = 20 + Math.random()*60;
el.style.setProperty('--dx', Math.cos(a)*d+'px');
el.style.setProperty('--dy', Math.sin(a)*d+'px');
el.style.setProperty('--rot', (Math.random()*360)+'deg');
scene.appendChild(el);
setTimeout(()=>el.remove(), 800);
}
/* ================= 主循环 ================= */
let lastT = performance.now();
function loop(now) {
const dt = Math.min((now - lastT) / 1000, 0.05);
lastT = now;
if (state.started && !state.over) {
state.ships.forEach(s => s.update(dt));
for (let i = state.projectiles.length - 1; i >= 0; i--) {
const p = state.projectiles[i];
p.x += p.vx * dt; p.y += p.vy * dt; p.life += dt;
p.el.style.transform = `translate3d(${p.x}px, ${p.y}px, 0) rotate(${p.angle}rad)`;
let hit = false;
if (p.life < p.maxLife) {
for (let s of state.ships) {
if (!s.alive || s.team === p.team) continue;
for (let m of s.modules) {
if (!m.alive) continue;
const cos = Math.cos(s.angle);
const sin = Math.sin(s.angle);
const mx = s.x + (m.def.x + m.def.w/2)*cos - (m.def.y + m.def.h/2)*sin;
const my = s.y + (m.def.x + m.def.w/2)*sin + (m.def.y + m.def.h/2)*cos;
if (pointInRotatedRect(p.x, p.y, mx, my, m.def.w, m.def.h, s.angle)) {
s.takeDamage(m, p.dmg, p.x, p.y);
hit = true; break;
}
}
if (hit) break;
if (p.type !== 'ciws') {
for (let q of state.projectiles) {
if (q.type === 'ciws' && q.team !== p.team) {
if (getDist(p.x, p.y, q.x, q.y) < 15) { hit = true; q.life = 99; break; }
}
}
}
}
} else hit = true;
if (hit) { p.el.remove(); state.projectiles.splice(i, 1); }
}
updateHUD();
}
requestAnimationFrame(loop);
}
function updateHUD() {
let hpA = 0, hpB = 0;
state.ships.forEach(s => {
s.modules.forEach(m => {
if (m.alive) { if(s.team==='A') hpA += m.hp; else hpB += m.hp; }
});
});
document.getElementById('hudA').style.width = Math.min(100, (hpA / 4000 * 100)) + '%';
document.getElementById('hudB').style.width = Math.min(100, (hpB / 4000 * 100)) + '%';
}
function checkGameOver() {
let aAlive = false, bAlive = false;
state.ships.forEach(s => { if (s.alive) { if (s.team === 'A') aAlive = true; else bAlive = true; } });
if (!aAlive || !bAlive) {
state.over = true;
setTimeout(() => { alert(aAlive ? "蓝方胜利" : (bAlive ? "红方胜利" : "同归于尽")); location.reload(); }, 800);
}
}
// 缩放拖拽
let pointers = new Map(), pinchD = 0;
function applyT() { scene.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; }
viewport.addEventListener('pointerdown', e => {
viewport.setPointerCapture(e.pointerId);
pointers.set(e.pointerId, {x: e.clientX, y: e.clientY});
if (pointers.size === 2) {
let pts = [...pointers.values()];
pinchD = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
}
});
viewport.addEventListener('pointermove', e => {
if (!pointers.has(e.pointerId)) return;
let p = pointers.get(e.pointerId);
let dx = e.clientX - p.x, dy = e.clientY - p.y;
p.x = e.clientX; p.y = e.clientY;
if (pointers.size === 1) {
tx += dx; ty += dy; applyT();
} else if (pointers.size === 2) {
let pts = [...pointers.values()];
let d = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
if (pinchD > 0) { scale *= (d / pinchD); scale = clamp(scale, 0.08, 1.5); applyT(); }
pinchD = d;
}
});
viewport.addEventListener('pointerup', e => { pointers.delete(e.pointerId); pinchD = 0; });
viewport.addEventListener('pointercancel', e => { pointers.delete(e.pointerId); pinchD = 0; });
// 开局配置
const counts = { A:{assault:1,laser:1,supply:1}, B:{assault:1,laser:1,supply:1} };
document.querySelectorAll('.btns button').forEach(btn => {
btn.onclick = () => {
let side = btn.dataset.side, type = btn.dataset.type, d = parseInt(btn.dataset.d);
counts[side][type] = clamp(counts[side][type] + d, 0, 4);
document.getElementById(`cnt${side}_${type}`).innerText = counts[side][type];
};
});
document.getElementById('startBtn').onclick = () => {
document.getElementById('overlay').classList.add('hide');
// 生成舰队 (X轴 1000 到 3000,Y轴居中在 2000)
let curY = 2000 - (counts.A.assault + counts.A.laser + counts.A.supply)*150 / 2;
for (let i=0; i<counts.A.assault; i++) { state.ships.push(new Ship('A','assault', 1000, curY)); curY+=150; }
for (let i=0; i<counts.A.laser; i++) { state.ships.push(new Ship('A','laser', 1000, curY)); curY+=150; }
for (let i=0; i<counts.A.supply; i++) { state.ships.push(new Ship('A','supply', 1000, curY)); curY+=150; }
curY = 2000 - (counts.B.assault + counts.B.laser + counts.B.supply)*150 / 2;
for (let i=0; i<counts.B.assault; i++) { state.ships.push(new Ship('B','assault', 3000, curY)); curY+=150; }
for (let i=0; i<counts.B.laser; i++) { state.ships.push(new Ship('B','laser', 3000, curY)); curY+=150; }
for (let i=0; i<counts.B.supply; i++) { state.ships.push(new Ship('B','supply', 3000, curY)); curY+=150; }
state.started = true;
// === 修复核心:视角完美居中在 2000, 2000 并缩放到刚好装下全场 ===
tx = 0;
ty = 0;
// 因为整个场景宽度是4000,我们要根据屏幕宽度算出合适的缩放
scale = Math.min(window.innerWidth / 2400, window.innerHeight / 2400);
applyT();
};
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 2036 舰队交战 - 完全体
Creator: ApexFalcon66
Libraries: none
Complexity: complex (697 lines, 33.5 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: 2036-apexfalcon66-mvm7zj5v" to link back to the original. Then publish at arcadelab.ai/publish.