星际雷霆 · 竖版卷轴飞行射击
by SuperOwl712245 lines66.0 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>
:root {
--accent: #4fc3f7;
--danger: #ff5252;
--gold: #ffd54f;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #03040a;
font-family: "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
color: #e6f1ff;
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
#game-container {
position: relative;
width: 100%;
max-width: 520px;
height: 100%;
margin: 0 auto;
overflow: hidden;
box-shadow: 0 0 70px rgba(0, 0, 0, 0.85);
}
canvas#game-canvas {
position: absolute;
inset: 0;
display: block;
}
/* ===== HUD ===== */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 18px 22px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
z-index: 10;
}
.hud-panel {
background: rgba(6, 12, 28, 0.55);
border: 1px solid rgba(79, 195, 247, 0.35);
border-radius: 12px;
padding: 10px 16px;
backdrop-filter: blur(6px);
box-shadow: 0 0 18px rgba(79, 195, 247, 0.18);
}
.hud-label {
font-size: 11px;
letter-spacing: 2px;
color: #8fb8d8;
text-transform: uppercase;
}
.hud-value {
font-size: 26px;
font-weight: 700;
color: #ffffff;
text-shadow: 0 0 12px rgba(79, 195, 247, 0.8);
font-variant-numeric: tabular-nums;
}
.hud-sub {
font-size: 12px;
color: #9be7ff;
letter-spacing: 1px;
margin-top: 2px;
}
#combo-value {
color: #ffd54f;
font-weight: 700;
transition: opacity 0.2s ease;
}
#lives .hud-value {
color: #ffd54f;
text-shadow: 0 0 12px rgba(255, 213, 79, 0.8);
}
/* ===== 提示浮字 ===== */
#toast {
position: absolute;
left: 50%;
top: 22%;
transform: translateX(-50%);
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
color: #ffffff;
text-shadow: 0 0 16px rgba(79, 195, 247, 0.9);
pointer-events: none;
opacity: 0;
z-index: 12;
transition: opacity 0.25s ease;
white-space: nowrap;
}
#toast.show { opacity: 1; }
/* ===== 暂停按钮 ===== */
#btn-pause {
position: absolute;
top: 74px;
right: 22px;
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid rgba(79, 195, 247, 0.4);
background: rgba(6, 12, 28, 0.55);
color: #e6f1ff;
font-size: 17px;
line-height: 1;
cursor: pointer;
z-index: 15;
backdrop-filter: blur(6px);
box-shadow: 0 0 12px rgba(79, 195, 247, 0.25);
}
/* ===== 暂停遮罩 ===== */
#pause-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 12px;
background: rgba(3, 4, 10, 0.72);
z-index: 18;
backdrop-filter: blur(3px);
cursor: pointer;
}
#pause-overlay.hidden { display: none; }
#pause-overlay .pause-title {
font-size: 42px;
font-weight: 900;
letter-spacing: 8px;
color: #ffffff;
text-shadow: 0 0 24px rgba(79, 195, 247, 0.8);
}
#pause-overlay .pause-hint {
font-size: 14px;
color: #8fb8d8;
letter-spacing: 2px;
}
/* ===== 触控控件(默认隐藏,触屏设备显示) ===== */
#joystick-base, #fire-btn { display: none; }
body.touch #joystick-base { display: block; }
body.touch #fire-btn { display: block; }
#joystick-base {
position: absolute;
left: 26px;
bottom: 30px;
width: 118px;
height: 118px;
border-radius: 50%;
border: 2px solid rgba(79, 195, 247, 0.4);
background: rgba(6, 12, 28, 0.32);
z-index: 15;
touch-action: none;
}
#joystick-knob {
position: absolute;
left: 50%;
top: 50%;
width: 52px;
height: 52px;
margin: -26px 0 0 -26px;
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, rgba(122, 220, 255, 0.9), rgba(21, 101, 192, 0.85));
box-shadow: 0 0 16px rgba(79, 195, 247, 0.6);
pointer-events: none;
}
#fire-btn {
position: absolute;
right: 26px;
bottom: 30px;
width: 98px;
height: 98px;
border-radius: 50%;
border: 2px solid rgba(255, 82, 82, 0.5);
background: radial-gradient(circle at 40% 35%, rgba(255, 138, 128, 0.72), rgba(198, 40, 40, 0.68));
color: #ffffff;
font-size: 17px;
font-weight: 700;
letter-spacing: 2px;
z-index: 15;
touch-action: none;
box-shadow: 0 0 20px rgba(255, 82, 82, 0.4);
}
#fire-btn:active { transform: scale(0.92); }
/* ===== 遮罩层(开始 / 结束) ===== */
#overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 14px;
background: radial-gradient(ellipse at center, rgba(10, 18, 40, 0.72) 0%, rgba(3, 4, 10, 0.92) 100%);
z-index: 20;
text-align: center;
padding: 20px;
transition: opacity 0.35s ease;
}
#overlay.hidden {
opacity: 0;
pointer-events: none;
}
#overlay .title {
font-size: clamp(40px, 8vw, 72px);
font-weight: 900;
letter-spacing: 6px;
background: linear-gradient(180deg, #7adcff, #2f7bff);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 40px rgba(47, 123, 255, 0.45);
filter: drop-shadow(0 0 14px rgba(47, 123, 255, 0.5));
}
#overlay .subtitle {
font-size: 15px;
letter-spacing: 4px;
color: #8fb8d8;
}
#overlay .final-score {
font-size: 30px;
font-weight: 800;
color: #ffd54f;
text-shadow: 0 0 20px rgba(255, 213, 79, 0.7);
}
#overlay .record {
font-size: 14px;
color: #9fe6c0;
}
#overlay .controls {
margin-top: 8px;
font-size: 13px;
color: #b9d3e8;
line-height: 1.9;
}
#overlay .controls b {
color: #ffffff;
background: rgba(79, 195, 247, 0.18);
border: 1px solid rgba(79, 195, 247, 0.4);
border-radius: 6px;
padding: 2px 8px;
margin: 0 2px;
font-weight: 600;
}
#overlay .legend {
margin-top: 6px;
font-size: 12px;
color: #8fb8d8;
line-height: 1.8;
}
#overlay .legend span {
display: inline-block;
margin: 0 4px;
}
#overlay .legend .s { color: #ffd54f; }
#overlay .legend .d { color: #ff8a80; }
#overlay .legend .w { color: #7adcff; }
#overlay .legend .li { color: #69f0ae; }
#overlay .legend .sc { color: #9fe6c0; }
#overlay .legend .bo { color: #7adcff; }
#overlay .touch-hint { display: none; font-size: 13px; color: #b9d3e8; }
body.touch #overlay .controls { display: none; }
body.touch #overlay .touch-hint { display: block; }
#btn-start {
margin-top: 16px;
font-size: 20px;
font-weight: 700;
letter-spacing: 3px;
color: #03121f;
background: linear-gradient(180deg, #7adcff, #2f7bff);
border: none;
border-radius: 40px;
padding: 14px 46px;
cursor: pointer;
box-shadow: 0 0 28px rgba(79, 195, 247, 0.6);
transition: transform 0.12s ease, box-shadow 0.2s ease;
}
#btn-start:hover {
transform: translateY(-2px) scale(1.03);
box-shadow: 0 0 38px rgba(79, 195, 247, 0.85);
}
#btn-start:active {
transform: scale(0.97);
}
/* ===== 响应式:小屏设备微调 ===== */
@media (max-width: 600px) {
#joystick-base {
width: 104px;
height: 104px;
left: 18px;
bottom: 22px;
}
#joystick-knob {
width: 46px;
height: 46px;
margin: -23px 0 0 -23px;
}
#fire-btn {
width: 84px;
height: 84px;
right: 18px;
bottom: 22px;
font-size: 15px;
}
#hud {
padding: 12px 14px;
}
.hud-panel {
padding: 7px 11px;
}
.hud-value {
font-size: 20px;
}
}
</style>
</head>
<body>
<div id="game-container">
<canvas id="game-canvas"></canvas>
<div id="hud">
<div class="hud-panel">
<div class="hud-label">分数 SCORE</div>
<div class="hud-value" id="score">0</div>
<div class="hud-sub" id="combo-value">连击 ×0</div>
</div>
<div class="hud-panel" id="weapon">
<div class="hud-label">火力 LEVEL</div>
<div class="hud-value" id="level-value">Lv.1</div>
<div class="hud-sub" id="wingmen-value">僚机 ×0</div>
</div>
<div class="hud-panel" id="lives">
<div class="hud-label">生命 LIVES</div>
<div class="hud-value" id="lives-value">❤❤❤</div>
</div>
</div>
<div id="toast"></div>
<button id="btn-pause" title="暂停 (P)">⏸</button>
<div id="joystick-base"><div id="joystick-knob"></div></div>
<button id="fire-btn">火力</button>
<div id="pause-overlay" class="hidden">
<div class="pause-title">已暂停</div>
<div class="pause-hint">按 P 键 / 点击此处继续</div>
</div>
<div id="overlay">
<div class="title">星际雷霆</div>
<div class="subtitle">STAR THUNDER · 竖版卷轴射击</div>
<div class="controls">
<b>W A S D</b> / <b>方向键</b> 移动 <b>空格</b> 射击 <b>P</b> 暂停<br>
击落敌机 · 收集道具 · 挑战 Boss · 累积连击
</div>
<div class="touch-hint">左下摇杆移动 · 右下按钮射击</div>
<div class="legend">
<span class="s"><b>S</b> 火力升级</span>
<span class="d"><b>D</b> 火力降级</span>
<span class="w"><b>W</b> 僚机</span>
<span class="li"><b>♥</b> 生命</span>
<span class="sc"><b>+</b> 加分</span>
<span class="bo"><b>✹</b> 清屏</span>
</div>
<button id="btn-start">开始游戏</button>
</div>
</div>
<script>
(function () {
"use strict";
/* ================= 基础设置 ================= */
const canvas = document.getElementById("game-canvas");
const ctx = canvas.getContext("2d");
const container = document.getElementById("game-container");
const scoreEl = document.getElementById("score");
const livesEl = document.getElementById("lives-value");
const levelEl = document.getElementById("level-value");
const wingmenEl = document.getElementById("wingmen-value");
const comboEl = document.getElementById("combo-value");
const toastEl = document.getElementById("toast");
const overlayEl = document.getElementById("overlay");
const btnStart = document.getElementById("btn-start");
const btnPause = document.getElementById("btn-pause");
const pauseOverlayEl = document.getElementById("pause-overlay");
const joystickBase = document.getElementById("joystick-base");
const joystickKnob = document.getElementById("joystick-knob");
const fireBtn = document.getElementById("fire-btn");
const HEARTS = ["❤", "❤", "❤", "❤", "❤"];
const PLAYER_BODY_A = "#1E90FF";
const PLAYER_BODY_B = "#00BFFF";
const LEVEL_HALO = {
1: { color: "#4fc3f7", name: "Lv.1" },
2: { color: "#26c6da", name: "Lv.2" },
3: { color: "#b388ff", name: "Lv.3" }
};
const COMBO_WINDOW = 3.0;
const MAX_PARTICLES = 500; // 同屏粒子上限
let particleScale = 1; // 自适应粒子密度(卡顿时自动降低)
let W = 0, H = 0;
function resize() {
W = canvas.width = container.clientWidth;
H = canvas.height = container.clientHeight;
}
window.addEventListener("resize", resize);
resize();
const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
if (isTouch) document.body.classList.add("touch");
/* ================= 音效(WebAudio 合成,无外部文件) ================= */
let audioCtx = null;
function ensureAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch (e) { audioCtx = null; }
}
if (audioCtx && audioCtx.state === "suspended") audioCtx.resume();
}
function tone(freq, dur, type, vol, slideTo) {
if (!audioCtx) return;
try {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type || "square";
o.frequency.setValueAtTime(freq, audioCtx.currentTime);
if (slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, audioCtx.currentTime + dur);
g.gain.setValueAtTime(vol || 0.05, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + dur);
o.connect(g).connect(audioCtx.destination);
o.start();
o.stop(audioCtx.currentTime + dur);
} catch (e) { /* 忽略音频错误 */ }
}
const sfx = {
shoot() { tone(880, 0.07, "square", 0.025, 500); }, // 能量弹
laser() { tone(1200, 0.13, "sawtooth", 0.04, 300); }, // 菱形激光
wheel() { tone(320, 0.16, "triangle", 0.05, 150); }, // 旋转光轮
homing() { tone(500, 0.16, "square", 0.04, 900); }, // 追踪弹
fan() { tone(700, 0.08, "square", 0.04, 520); tone(900, 0.09, "square", 0.04, 620); }, // 扇形弹
spiral() { tone(420, 0.28, "sawtooth", 0.04, 190); }, // 螺旋弹
explode() { tone(160, 0.28, "sawtooth", 0.07, 40); },
powerup() { tone(520, 0.14, "triangle", 0.06); tone(780, 0.16, "triangle", 0.06, 1040); },
downgrade(){ tone(400, 0.2, "sawtooth", 0.06, 160); },
hit() { tone(200, 0.25, "sawtooth", 0.08, 60); },
combo() { tone(660, 0.12, "square", 0.06, 990); },
bossAlarm(){ tone(220, 0.5, "square", 0.08, 110); tone(330, 0.5, "square", 0.08, 165); },
gameover(){ tone(300, 0.6, "sawtooth", 0.06, 60); }
};
/* ================= 游戏状态 ================= */
let state = "menu"; // menu | playing | over
let paused = false;
let score = 0;
let best = 0;
let bossProgress = 0;
const BOSS_EVERY = 500;
let combo = 0;
let comboTimer = 0;
let comboPopupData = null;
try { best = parseInt(localStorage.getItem("starThunderBest") || "0", 10) || 0; } catch (e) {}
const player = {
x: W / 2, y: H * 0.8,
r: 16,
speed: 340,
lives: 3,
level: 1,
invincible: 0,
fireCooldown: 0,
alive: true
};
let bullets = [];
let enemies = [];
let enemyBullets = [];
let powerups = [];
let wingmen = [];
let boss = null;
let particles = [];
let rings = [];
let stars = [];
let floatingTexts = [];
let spawnTimer = 0;
let spawnInterval = 1.15;
let elapsed = 0;
let shake = 0;
let toastTimer = 0;
let freezeTimer = 0; // 顿帧(阶段切换)
let phase2Popup = null; // "Phase 2" 提示
let bossAlertTimer = 0; // 暴走红光强度
/* ================= 输入(键盘) ================= */
const keys = new Set();
window.addEventListener("keydown", (e) => {
const k = e.key.toLowerCase();
if ([" ", "arrowup", "arrowdown", "arrowleft", "arrowright"].includes(k)) {
e.preventDefault();
}
if (k === "p" || k === "escape") {
if (state === "playing") togglePause();
return;
}
keys.add(k);
if ((k === " " || k === "enter") && state === "over") startGame();
if ((k === " " || k === "enter") && state === "menu") startGame();
});
window.addEventListener("keyup", (e) => {
keys.delete(e.key.toLowerCase());
});
window.addEventListener("blur", () => keys.clear());
btnStart.addEventListener("click", () => {
ensureAudio();
if (state === "menu" || state === "over") startGame();
});
btnPause.addEventListener("click", () => {
if (state === "playing") togglePause();
});
pauseOverlayEl.addEventListener("click", () => {
if (state === "playing") togglePause();
});
/* ================= 触控输入 ================= */
let moveVector = { x: 0, y: 0 };
let fireHeld = false;
let joyId = null;
const JOY_RADIUS = 46;
function joyCenter() {
const r = joystickBase.getBoundingClientRect();
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
}
function joyUpdate(t) {
const c = joyCenter();
let dx = t.clientX - c.x, dy = t.clientY - c.y;
const d = Math.hypot(dx, dy);
if (d > JOY_RADIUS) { dx = dx / d * JOY_RADIUS; dy = dy / d * JOY_RADIUS; }
joystickKnob.style.transform = "translate(" + dx + "px," + dy + "px)";
moveVector.x = dx / JOY_RADIUS;
moveVector.y = dy / JOY_RADIUS;
}
function joyReset() {
joyId = null;
joystickKnob.style.transform = "translate(0px,0px)";
moveVector.x = 0; moveVector.y = 0;
}
function findTouch(e, id) {
for (let i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === id) return e.touches[i];
}
return null;
}
joystickBase.addEventListener("touchstart", (e) => {
e.preventDefault();
if (joyId !== null) return;
const t = e.changedTouches[0];
joyId = t.identifier;
joyUpdate(t);
}, { passive: false });
joystickBase.addEventListener("touchmove", (e) => {
e.preventDefault();
const t = findTouch(e, joyId);
if (t) joyUpdate(t);
}, { passive: false });
joystickBase.addEventListener("touchend", (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === joyId) joyReset();
}
});
joystickBase.addEventListener("touchcancel", joyReset);
fireBtn.addEventListener("touchstart", (e) => {
e.preventDefault();
ensureAudio();
fireHeld = true;
}, { passive: false });
fireBtn.addEventListener("touchend", () => { fireHeld = false; });
fireBtn.addEventListener("touchcancel", () => { fireHeld = false; });
/* ================= 星空背景 ================= */
function initStars() {
stars = [];
const count = Math.floor((W * H) / 9000);
for (let i = 0; i < count; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: Math.random() * 1.8 + 0.4,
speed: Math.random() * 60 + 25,
tw: Math.random() * Math.PI * 2
});
}
}
initStars();
/* ================= 工具函数 ================= */
const rand = (a, b) => a + Math.random() * (b - a);
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function circlesHit(a, b) {
const r = (a.r || 8) + (b.r || 8);
return dist(a, b) < r;
}
function showToast(text, color) {
toastEl.textContent = text;
toastEl.style.color = color || "#ffffff";
toastEl.classList.add("show");
toastTimer = 1.8;
}
function addFloatingText(x, y, text, color, size) {
floatingTexts.push({ x, y, text, color: color || "#ffffff", size: size || 16, life: 1.1, maxLife: 1.1 });
}
function addParticle(p) {
if (particles.length >= MAX_PARTICLES) return;
particles.push(p);
}
function burst(x, y, color, n, speed, size) {
n = Math.round(n * particleScale);
for (let i = 0; i < n; i++) {
const ang = Math.random() * Math.PI * 2;
const sp = rand(speed * 0.3, speed);
addParticle({
x, y,
vx: Math.cos(ang) * sp,
vy: Math.sin(ang) * sp,
r: rand(size * 0.4, size),
color,
life: rand(0.3, 0.8),
maxLife: 0.8,
drag: 0.985
});
}
}
// 子弹命中:散射小碎片
function sparkHit(x, y, color) {
const n = Math.max(2, Math.round(4 * particleScale));
for (let i = 0; i < n; i++) {
const ang = Math.random() * Math.PI * 2;
const sp = rand(70, 170);
addParticle({
x, y,
vx: Math.cos(ang) * sp,
vy: Math.sin(ang) * sp,
r: rand(1, 2.4),
color: color || "#ffffff",
life: rand(0.2, 0.42),
maxLife: 0.42,
drag: 0.94
});
}
}
function addRing(x, y, color, maxR) {
rings.push({ x, y, r: 4, maxR: maxR || 70, color, life: 0.5, maxLife: 0.5 });
}
function levelHalo() { return LEVEL_HALO[player.level] || LEVEL_HALO[1]; }
function ddsFactor() { return 1 + score / 2000; }
function comboMultiplier() {
return Math.min(3, 1 + combo * 0.05);
}
function addScore(n) {
score += n;
bossProgress += n;
maybeSpawnBoss();
}
function maybeSpawnBoss() {
while (!boss && bossProgress >= BOSS_EVERY) {
bossProgress -= BOSS_EVERY;
spawnBoss();
}
}
function triggerComboPopup() {
comboPopupData = { text: "COMBO ×" + combo, life: 1.5, maxLife: 1.5 };
shake = 8;
sfx.combo();
}
/* ================= 弹幕发射 ================= */
// 扇形弹:一次性射出 count 颗呈扇形
function fireFan(x, y, count, spread, speed, color, kind) {
const step = count > 1 ? spread / (count - 1) : 0;
const base = Math.PI / 2;
for (let i = 0; i < count; i++) {
const ang = base - spread / 2 + step * i;
enemyBullets.push({
x, y, r: 5,
vx: Math.cos(ang) * speed, vy: Math.sin(ang) * speed,
kind: kind || "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
color: color || "#7adcff"
});
}
}
// 螺旋弹:围绕中心旋转扩散
function fireSpiral(x, y, count, radiusSpeed, angSpeed, color) {
for (let i = 0; i < count; i++) {
enemyBullets.push({
x, y, r: 6,
cx: x, cy: y,
angle: (Math.PI * 2 / count) * i,
radius: 10,
radiusSpeed: radiusSpeed || 55,
angSpeed: angSpeed || 2.4,
kind: "wheel", pattern: "spiral", spin: rand(0, Math.PI * 2),
color: color || "#ffd54f", life: 8
});
}
}
// 追踪弹:缓慢转向玩家
function fireHoming(x, y, speed, color) {
enemyBullets.push({
x, y, r: 5,
vx: 0, vy: speed || 150,
turnRate: 3.0,
kind: "laser", pattern: "homing", spin: 0,
color: color || "#ff8a50", life: 3
});
}
/* ================= 实体工厂 ================= */
function spawnEnemy() {
const diff = Math.min(1, score / 3000);
const dds = ddsFactor();
const speedMult = Math.min(dds, 2.0);
const types = ["straight", "straight", "sine", "fast", "zigzag", "tank", "kamikaze", "transporter"];
let type = types[Math.floor(Math.random() * types.length)];
if (diff < 0.15 && (type === "tank" || type === "zigzag" || type === "kamikaze" || type === "transporter")) {
type = "straight";
}
const base = {
x: rand(W * 0.08, W * 0.92),
y: -30,
r: 16,
hp: 1,
speed: rand(90, 150) + diff * 90,
type,
phase: rand(0, Math.PI * 2),
amp: rand(30, 90),
shootTimer: rand(1, 3),
flash: 0,
worth: 100
};
switch (type) {
case "sine":
base.r = 14; base.speed = rand(70, 110) + diff * 60; base.worth = 150;
break;
case "fast":
base.r = 12; base.speed = rand(220, 300) + diff * 100; base.worth = 200;
break;
case "zigzag":
base.r = 14; base.speed = rand(130, 190) + diff * 70; base.worth = 180;
base.vx = (Math.random() < 0.5 ? -1 : 1) * rand(90, 150);
base.dirTimer = rand(0.6, 1.2);
break;
case "tank":
base.r = 24; base.hp = 3; base.speed = rand(40, 70) + diff * 30; base.worth = 400;
break;
case "kamikaze":
base.r = 13; base.hp = 1; base.speed = rand(300, 420); base.worth = 180;
base.x = clamp(player.x + rand(-40, 40), 16, W - 16);
base.aim = true;
break;
case "transporter":
base.r = 20; base.hp = 2; base.worth = 250;
base.vx = (Math.random() < 0.5 ? -1 : 1) * rand(80, 130);
base.vy = rand(30, 55);
base.shootTimer = null;
break;
default:
base.r = 15; base.speed = rand(90, 150) + diff * 90; base.worth = 100;
}
base.hp = Math.max(1, Math.round(base.hp * dds));
base.maxHp = base.hp;
base.speed *= speedMult;
if (base.vx !== undefined) base.vx *= speedMult;
if (base.vy !== undefined) base.vy *= speedMult;
enemies.push(base);
}
function spawnPowerup(x, y, forcedType) {
let type = forcedType;
if (!type) {
const roll = Math.random();
if (roll < 0.28) type = "score";
else if (roll < 0.46) type = "upgrade";
else if (roll < 0.56) type = "wingman";
else if (roll < 0.66) type = "life";
else if (roll < 0.74) type = "bomb";
else if (roll < 0.90) type = "downgrade";
else type = "score";
}
powerups.push({
x, y, r: 12,
type,
vy: rand(70, 100),
phase: rand(0, Math.PI * 2),
life: 10
});
}
function spawnBoss() {
const dds = ddsFactor();
const maxHp = Math.max(45, Math.round(45 * dds));
boss = {
x: W / 2, y: -90,
r: 46,
hp: maxHp, maxHp: maxHp,
targetY: 110,
phase: 1, // 1 | 2
entering: true,
sineT: 0,
ringTimer: 1.2, // 圆形扩散弹
aimTimer: 2.4, // 瞄准三连射
fanTimer: 1.1, // 扇形密集弹
dashSpeed: 420, // 阶段2 冲刺速度
dashState: "pause",// 冲刺状态机 dash | pause
dashDir: 0, // 冲刺锁定方向
dashTimer: 0, // 当前状态剩余时间
laserState: "idle",// idle | charging | firing
laserX: 0,
laserTimer: 0,
flash: 0
};
showToast("⚠ BOSS 来袭!", "#c9a0ff");
sfx.bossAlarm();
shake = 12;
}
function addWingmen() {
wingmen = [];
wingmen.push({ angle: 0, r: 10, x: player.x, y: player.y, fireCooldown: 0 });
wingmen.push({ angle: Math.PI, r: 10, x: player.x, y: player.y, fireCooldown: 0 });
}
/* ================= 开始 / 暂停 / 重开 ================= */
function togglePause() {
paused = !paused;
pauseOverlayEl.classList.toggle("hidden", !paused);
btnPause.textContent = paused ? "▶" : "⏸";
}
function startGame() {
ensureAudio();
state = "playing";
paused = false;
pauseOverlayEl.classList.add("hidden");
btnPause.textContent = "⏸";
score = 0;
bossProgress = 0;
combo = 0;
comboTimer = 0;
comboPopupData = null;
elapsed = 0;
spawnTimer = 0.8;
spawnInterval = 1.15;
shake = 0;
freezeTimer = 0;
phase2Popup = null;
bossAlertTimer = 0;
player.x = W / 2;
player.y = H * 0.8;
player.lives = 3;
player.level = 1;
player.invincible = 0;
player.fireCooldown = 0;
player.alive = true;
bullets = []; enemies = []; enemyBullets = []; powerups = [];
wingmen = []; boss = null; particles = []; rings = []; floatingTexts = [];
overlayEl.classList.add("hidden");
updateHUD();
showToast("战斗开始!", "#7adcff");
}
function gameOver() {
state = "over";
paused = false;
pauseOverlayEl.classList.add("hidden");
sfx.gameover();
burst(player.x, player.y, "#00BFFF", 40, 320, 4);
burst(player.x, player.y, "#ffffff", 20, 220, 3);
addRing(player.x, player.y, "#00BFFF", 120);
if (score > best) {
best = score;
try { localStorage.setItem("starThunderBest", String(best)); } catch (e) {}
}
document.querySelector("#overlay .title").textContent = "Game Over";
document.querySelector("#overlay .subtitle").textContent = "GAME OVER";
document.querySelector("#overlay .final-score")?.remove();
document.querySelector("#overlay .record")?.remove();
const controls = document.querySelector("#overlay .controls");
const fs = document.createElement("div");
fs.className = "final-score";
fs.textContent = "本局得分 " + score;
const rec = document.createElement("div");
rec.className = "record";
rec.textContent = "最高分 " + best;
overlayEl.insertBefore(rec, controls);
overlayEl.insertBefore(fs, rec);
btnStart.textContent = "再来一局";
overlayEl.classList.remove("hidden");
}
function backToMenu() {
state = "menu";
document.querySelector("#overlay .title").textContent = "星际雷霆";
document.querySelector("#overlay .subtitle").textContent = "STAR THUNDER · 竖版卷轴射击";
document.querySelector("#overlay .final-score")?.remove();
document.querySelector("#overlay .record")?.remove();
btnStart.textContent = "开始游戏";
overlayEl.classList.remove("hidden");
}
function updateHUD() {
scoreEl.textContent = String(score);
livesEl.textContent = HEARTS.slice(0, Math.max(0, player.lives)).join("");
levelEl.textContent = levelHalo().name;
levelEl.style.color = levelHalo().color;
wingmenEl.textContent = "僚机 ×" + wingmen.length;
if (combo >= 2) {
comboEl.textContent = "连击 ×" + combo + " +" + Math.round((comboMultiplier() - 1) * 100) + "%";
comboEl.style.opacity = "1";
} else {
comboEl.style.opacity = "0";
}
}
/* ================= 更新逻辑 ================= */
function update(dt) {
if (state !== "playing") return;
elapsed += dt;
if (shake > 0) shake = Math.max(0, shake - dt * 30);
if (toastTimer > 0) {
toastTimer -= dt;
if (toastTimer <= 0) toastEl.classList.remove("show");
}
if (combo > 0) {
comboTimer -= dt;
if (comboTimer <= 0) combo = 0;
}
if (comboPopupData) {
comboPopupData.life -= dt;
if (comboPopupData.life <= 0) comboPopupData = null;
}
/* --- 星空 --- */
for (const s of stars) {
s.y += s.speed * dt;
s.tw += dt * 3;
if (s.y > H + 5) { s.y = -5; s.x = Math.random() * W; }
}
/* --- 玩家移动(键盘 + 触控摇杆) --- */
if (player.alive) {
let dx = 0, dy = 0;
const jm = Math.hypot(moveVector.x, moveVector.y);
if (jm > 0.15) {
dx = moveVector.x;
dy = moveVector.y;
} else {
if (keys.has("a") || keys.has("arrowleft")) dx -= 1;
if (keys.has("d") || keys.has("arrowright")) dx += 1;
if (keys.has("w") || keys.has("arrowup")) dy -= 1;
if (keys.has("s") || keys.has("arrowdown")) dy += 1;
if (dx !== 0 && dy !== 0) { dx *= 0.7071; dy *= 0.7071; }
}
player.x = clamp(player.x + dx * player.speed * dt, 22, W - 22);
player.y = clamp(player.y + dy * player.speed * dt, 40, H - 30);
const halo = levelHalo().color;
if (Math.random() < 0.9 * particleScale) {
for (let n = 0; n < 2; n++) {
const nx = player.x + (n === 0 ? -4 : 4);
addParticle({
x: nx + rand(-1, 1), y: player.y + player.r * 0.9,
vx: rand(-10, 10), vy: rand(140, 260),
r: rand(1.2, 2.8),
color: Math.random() < 0.6 ? "#ffffff" : halo,
life: rand(0.2, 0.45), maxLife: 0.45, drag: 0.96
});
}
}
player.fireCooldown -= dt;
if ((fireHeld || keys.has(" ")) && player.fireCooldown <= 0) {
fire();
player.fireCooldown = 0.16;
}
const orbit = 34;
for (const w of wingmen) {
w.angle += dt * 2.4;
w.x = player.x + Math.cos(w.angle) * orbit;
w.y = player.y + Math.sin(w.angle) * orbit * 0.55;
w.fireCooldown -= dt;
if (w.fireCooldown <= 0) {
w.fireCooldown = 0.4;
bullets.push({ x: w.x, y: w.y - 8, vx: 0, vy: -460, r: 3, kind: "energy", pattern: "normal", spin: 0, color: "#7adcff", pierce: false });
sfx.shoot();
}
}
}
if (player.invincible > 0) player.invincible -= dt;
/* --- 敌人生成(Boss 战中减半) --- */
spawnTimer -= dt;
const diff = Math.min(1, score / 3000);
spawnInterval = Math.max(0.34, 1.15 - diff * 0.8) * (boss ? 2 : 1);
if (spawnTimer <= 0) {
spawnEnemy();
if (diff > 0.5 && Math.random() < 0.3) spawnEnemy();
spawnTimer = spawnInterval * rand(0.8, 1.2);
}
/* --- 敌人更新 --- */
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
e.phase += dt * 3;
if (e.flash > 0) e.flash -= dt;
switch (e.type) {
case "sine":
e.y += e.speed * dt;
e.x += Math.sin(e.phase) * 120 * dt;
break;
case "zigzag":
e.y += e.speed * dt;
e.dirTimer -= dt;
if (e.dirTimer <= 0) { e.vx *= -1; e.dirTimer = rand(0.6, 1.2); }
e.x += e.vx * dt;
break;
case "kamikaze":
e.y += e.speed * dt;
e.x += clamp(player.x - e.x, -120, 120) * dt;
break;
case "transporter":
e.y += e.vy * dt;
e.x += e.vx * dt;
if (e.x < 16 || e.x > W - 16) e.vx *= -1;
break;
default:
e.y += e.speed * dt;
}
e.x = clamp(e.x, 16, W - 16);
// 敌方射击
if ((e.type === "straight" || e.type === "tank") && e.shootTimer !== null) {
e.shootTimer -= dt;
if (e.shootTimer <= 0 && e.y < H * 0.6) {
if (e.type === "tank") {
// 精英:追踪激光
fireHoming(e.x, e.y + e.r, 150, "#ff8a50");
sfx.homing();
e.shootTimer = 2.0;
} else {
// 普通:圆形能量弹
enemyBullets.push({ x: e.x, y: e.y + e.r, r: 4, vx: 0, vy: 200, kind: "energy", pattern: "normal", spin: 0, color: "#7adcff" });
sfx.shoot();
e.shootTimer = rand(2, 3.4);
}
}
}
if (e.y > H + 40) { enemies.splice(i, 1); }
}
/* --- Boss 更新(多阶段动态 AI) --- */
if (boss) {
if (boss.flash > 0) boss.flash -= dt;
if (bossAlertTimer > 0) bossAlertTimer -= dt;
// 阶段切换:血量 <= 50%
if (boss.phase === 1 && boss.hp <= boss.maxHp / 2) enterPhase2();
if (boss.entering) {
boss.y += 90 * dt;
if (boss.y >= boss.targetY) { boss.y = boss.targetY; boss.entering = false; }
} else if (boss.phase === 1) {
// 第一阶段:缓慢 S 形正弦移动
boss.sineT += dt * 0.9;
boss.x = W / 2 + Math.sin(boss.sineT) * (W * 0.32);
boss.y = boss.targetY + Math.sin(boss.sineT * 2) * 26;
boss.x = clamp(boss.x, boss.r + 10, W - boss.r - 10);
boss.ringTimer -= dt;
if (boss.ringTimer <= 0) { bossRing(); boss.ringTimer = 1.5; }
boss.aimTimer -= dt;
if (boss.aimTimer <= 0) { bossAim(); boss.aimTimer = 2.2; }
} else {
// 第二阶段:暴走,水平冲刺追击(锁定方向 + 停顿,避免与玩家同步)
boss.sineT += dt * 2;
boss.y = boss.targetY + Math.sin(boss.sineT) * 12;
boss.dashTimer -= dt;
if (boss.dashState === "dash") {
// 冲刺中:按锁定方向移动,不重新索敌
boss.x += boss.dashDir * boss.dashSpeed * dt;
if (boss.dashTimer <= 0) {
boss.dashState = "pause";
boss.dashTimer = 0.6;
}
} else {
// 停顿期:横向静止,到点后重新锁定玩家方位冲刺
if (boss.dashTimer <= 0) {
boss.dashState = "dash";
boss.dashDir = player.x >= boss.x ? 1 : -1;
boss.dashTimer = 0.38;
}
}
boss.x = clamp(boss.x, boss.r + 10, W - boss.r - 10);
if (boss.laserState === "charging") {
boss.laserTimer -= dt;
if (boss.laserTimer <= 0) {
boss.laserState = "firing";
boss.laserTimer = 0.8;
sfx.laser();
}
} else if (boss.laserState === "firing") {
boss.laserTimer -= dt;
if (player.alive && player.invincible <= 0 && Math.abs(player.x - boss.laserX) < 22 && player.y > boss.y - 12) {
hurtPlayer();
}
if (boss.laserTimer <= 0) boss.laserState = "idle";
} else {
boss.fanTimer -= dt;
if (boss.fanTimer <= 0) {
if (Math.random() < 0.5) {
bossFan();
boss.fanTimer = 1.1;
} else {
boss.laserState = "charging";
boss.laserX = player.x;
boss.laserTimer = 1.0;
sfx.homing();
boss.fanTimer = 1.6;
}
}
}
}
}
/* --- 玩家子弹更新 --- */
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.y < -20 || b.x < -20 || b.x > W + 20) bullets.splice(i, 1);
}
/* --- 敌方子弹更新(含追踪/螺旋弹道) --- */
for (let i = enemyBullets.length - 1; i >= 0; i--) {
const b = enemyBullets[i];
b.spin = (b.spin || 0) + (b.kind === "wheel" ? dt * 12 : 0);
if (b.pattern === "homing") {
const desired = Math.atan2(player.y - b.y, player.x - b.x);
const cur = Math.atan2(b.vy, b.vx);
let d = desired - cur;
while (d > Math.PI) d -= Math.PI * 2;
while (d < -Math.PI) d += Math.PI * 2;
const turn = clamp(d, -(b.turnRate || 3) * dt, (b.turnRate || 3) * dt);
const na = cur + turn;
const spd = Math.hypot(b.vx, b.vy) || 150;
b.vx = Math.cos(na) * spd;
b.vy = Math.sin(na) * spd;
b.x += b.vx * dt;
b.y += b.vy * dt;
} else if (b.pattern === "spiral") {
b.angle += b.angSpeed * dt;
b.radius += b.radiusSpeed * dt;
b.x = b.cx + Math.cos(b.angle) * b.radius;
b.y = b.cy + Math.sin(b.angle) * b.radius;
} else {
b.x += (b.vx || 0) * dt;
b.y += b.vy * dt;
}
if (b.life !== undefined) {
b.life -= dt;
if (b.life <= 0) { if (b.pattern === "homing") sparkHit(b.x, b.y, b.color); enemyBullets.splice(i, 1); continue; }
}
if (b.y > H + 40 || b.y < -40 || b.x < -40 || b.x > W + 40) enemyBullets.splice(i, 1);
}
/* --- 道具更新 --- */
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.y += p.vy * dt;
p.phase += dt * 4;
p.life -= dt;
if (p.y > H + 20 || p.life <= 0) powerups.splice(i, 1);
}
/* --- 碰撞:子弹 vs 敌人 / Boss --- */
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
let removed = false;
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (circlesHit(b, e)) {
e.hp -= 1;
e.flash = 0.08;
sparkHit(b.x, b.y, "#ffd54f");
burst(b.x, b.y, "#ffd54f", 5, 140, 2.2);
if (e.hp <= 0) destroyEnemy(e, j);
if (!b.pierce) { removed = true; break; }
}
}
if (!removed && boss && circlesHit(b, boss)) {
boss.hp -= 1;
boss.flash = 0.06;
sparkHit(b.x, b.y, "#c9a0ff");
burst(b.x, b.y, "#c9a0ff", 4, 120, 2);
if (!b.pierce) removed = true;
if (boss.hp <= 0) destroyBoss();
}
if (removed) bullets.splice(i, 1);
}
/* --- 碰撞:玩家 vs 敌人 / Boss / 敌方子弹 --- */
if (player.alive && player.invincible <= 0) {
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (circlesHit(player, e)) {
destroyEnemy(e, j);
hurtPlayer();
break;
}
}
if (player.alive && boss && circlesHit(player, boss)) {
hurtPlayer();
}
}
if (player.alive && player.invincible <= 0) {
for (let j = enemyBullets.length - 1; j >= 0; j--) {
const b = enemyBullets[j];
if (circlesHit(player, b)) {
sparkHit(b.x, b.y, b.color);
enemyBullets.splice(j, 1);
hurtPlayer();
break;
}
}
}
/* --- 碰撞:僚机 vs 敌人 / Boss / 敌方子弹 --- */
if (player.alive) {
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
for (let k = wingmen.length - 1; k >= 0; k--) {
if (circlesHit(e, wingmen[k])) {
destroyEnemy(e, j);
destroyWingman(k);
break;
}
}
}
if (boss) {
for (let k = wingmen.length - 1; k >= 0; k--) {
if (circlesHit(boss, wingmen[k])) destroyWingman(k);
}
}
for (let j = enemyBullets.length - 1; j >= 0; j--) {
const b = enemyBullets[j];
for (let k = wingmen.length - 1; k >= 0; k--) {
if (circlesHit(b, wingmen[k])) {
sparkHit(b.x, b.y, b.color);
enemyBullets.splice(j, 1);
destroyWingman(k);
break;
}
}
}
}
/* --- 碰撞:玩家 vs 道具 --- */
if (player.alive) {
for (let j = powerups.length - 1; j >= 0; j--) {
const p = powerups[j];
if (dist(player, p) < player.r + p.r + 6) {
applyPowerup(p);
powerups.splice(j, 1);
}
}
}
/* --- 粒子更新 --- */
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.vx *= p.drag;
p.vy *= p.drag;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
/* --- 冲击波环更新 --- */
for (let i = rings.length - 1; i >= 0; i--) {
const r = rings[i];
r.r += (r.maxR - r.r) * 8 * dt;
r.life -= dt;
if (r.life <= 0) rings.splice(i, 1);
}
/* --- 飘字更新 --- */
for (let i = floatingTexts.length - 1; i >= 0; i--) {
const f = floatingTexts[i];
f.y -= 40 * dt;
f.life -= dt;
if (f.life <= 0) floatingTexts.splice(i, 1);
}
}
/* ================= 战斗行为 ================= */
function fire() {
sfx.shoot();
const py = player.y - player.r;
const col = levelHalo().color;
const pierce = player.level === 3;
if (player.level === 1) {
bullets.push({ x: player.x, y: py, vx: 0, vy: -520, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
} else if (player.level === 2) {
bullets.push({ x: player.x - 8, y: py, vx: 0, vy: -540, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
bullets.push({ x: player.x + 8, y: py, vx: 0, vy: -540, r: 4, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: false });
} else {
bullets.push({ x: player.x - 12, y: py, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: true });
bullets.push({ x: player.x, y: py - 4, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: "#ffffff", pierce: true });
bullets.push({ x: player.x + 12, y: py, vx: 0, vy: -560, r: 5, kind: "energy", pattern: "normal", spin: 0, color: col, pierce: true });
}
}
// 第一阶段:圆形扩散弹(12 发,向四周)
function bossRing() {
const count = 12;
for (let i = 0; i < count; i++) {
const ang = (Math.PI * 2 / count) * i;
enemyBullets.push({
x: boss.x, y: boss.y, r: 5,
vx: Math.cos(ang) * 150, vy: Math.sin(ang) * 150,
kind: "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
color: "#7adcff", life: 6
});
}
sfx.fan();
}
// 第一阶段:瞄准三连射
function bossAim() {
const ang = Math.atan2(player.y - boss.y, player.x - boss.x);
for (let i = -1; i <= 1; i++) {
const a = ang + i * 0.09;
enemyBullets.push({
x: boss.x, y: boss.y, r: 5,
vx: Math.cos(a) * 280, vy: Math.sin(a) * 280,
kind: "laser", pattern: "normal", spin: 0,
color: "#ff8a50", life: 6
});
}
sfx.homing();
}
// 第二阶段:扇形密集弹(15 发 / 30 度)
function bossFan() {
const ang = Math.atan2(player.y - boss.y, player.x - boss.x);
const count = 15;
const spread = Math.PI / 6;
const step = spread / (count - 1);
for (let i = 0; i < count; i++) {
const a = ang - spread / 2 + step * i;
enemyBullets.push({
x: boss.x, y: boss.y, r: 4,
vx: Math.cos(a) * 230, vy: Math.sin(a) * 230,
kind: "energy", pattern: "normal", spin: rand(0, Math.PI * 2),
color: "#7adcff", life: 6
});
}
sfx.fan();
}
// 进入第二阶段(暴走)
function enterPhase2() {
boss.phase = 2;
boss.laserState = "idle";
boss.laserTimer = 0;
boss.fanTimer = 0.6;
freezeTimer = 0.6; // 顿帧
phase2Popup = { life: 2.2, maxLife: 2.2 };
bossAlertTimer = 2.0;
shake = 18;
sfx.bossAlarm();
burst(boss.x, boss.y, "#ff5252", 40, 420, 4);
addRing(boss.x, boss.y, "#ff5252", 220);
}
function destroyEnemy(e, index) {
enemies.splice(index, 1);
combo += 1;
comboTimer = COMBO_WINDOW;
const mult = comboMultiplier();
const gained = Math.round(e.worth * mult);
addScore(gained);
sfx.explode();
shake = 6;
const palette = e.type === "tank"
? ["#8A2BE2", "#c9a0ff", "#ffd54f", "#ffffff"]
: ["#ff5252", "#ff7043", "#ffd54f"];
burst(e.x, e.y, palette[0], 18, 260, 3.5);
burst(e.x, e.y, palette[1], 12, 200, 2.6);
addRing(e.x, e.y, palette[0], 46);
addFloatingText(e.x, e.y, "+" + gained + (combo >= 2 ? " ×" + mult.toFixed(1) : ""), "#ffd54f", 16);
if (combo > 0 && combo % 10 === 0) triggerComboPopup();
if (e.type === "transporter") {
addFloatingText(e.x, e.y - 26, "3 个道具!", "#26c6da", 16);
const goods = ["upgrade", "wingman", "life"];
for (let g = 0; g < 3; g++) {
spawnPowerup(e.x + rand(-36, 36), e.y + rand(-12, 12), goods[g]);
}
} else {
const dropChance = e.type === "tank" ? 0.45 : 0.14;
if (Math.random() < dropChance) spawnPowerup(e.x, e.y);
}
updateHUD();
}
function destroyBoss() {
const dds = ddsFactor();
score *= 2;
shake = 30;
sfx.explode();
sfx.powerup();
const bx = boss.x, by = boss.y;
burst(bx, by, "#8A2BE2", 60, 420, 5);
burst(bx, by, "#c9a0ff", 40, 320, 4);
burst(bx, by, "#ffffff", 30, 260, 3);
addRing(bx, by, "#8A2BE2", 180);
addRing(bx, by, "#c9a0ff", 120);
addFloatingText(bx, by - 20, "BOSS 击败!分数翻倍!", "#c9a0ff", 24);
const sChance = Math.min(0.8, 0.4 + (dds - 1) * 0.5);
for (let i = 0; i < 3; i++) {
const r = Math.random();
const type = r < sChance ? "upgrade" : (r < sChance + 0.25 ? "wingman" : "life");
spawnPowerup(bx + rand(-50, 50), by + rand(-20, 30), type);
}
boss = null;
updateHUD();
}
function destroyWingman(k) {
const w = wingmen[k];
wingmen.splice(k, 1);
burst(w.x, w.y, "#00BFFF", 14, 220, 2.6);
addRing(w.x, w.y, "#00BFFF", 40);
sfx.hit();
addFloatingText(w.x, w.y, "僚机损毁!", "#9be7ff", 14);
updateHUD();
}
function hurtPlayer() {
player.lives -= 1;
player.level = 1;
player.invincible = 2.2;
combo = 0;
shake = 14;
sfx.hit();
burst(player.x, player.y, levelHalo().color, 26, 280, 3.5);
burst(player.x, player.y, "#ff5252", 16, 220, 3);
addRing(player.x, player.y, "#ff5252", 90);
addFloatingText(player.x, player.y - 30, "命中!", "#ff8a80", 18);
updateHUD();
if (player.lives <= 0) {
player.alive = false;
gameOver();
}
}
function applyPowerup(p) {
let msg, color;
switch (p.type) {
case "upgrade":
player.level = Math.min(3, player.level + 1);
msg = "火力升级 " + levelHalo().name + "!"; color = "#ffd54f";
sfx.powerup();
break;
case "downgrade":
player.level = Math.max(1, player.level - 1);
msg = "火力降级 " + levelHalo().name; color = "#ff8a80";
sfx.downgrade();
break;
case "wingman":
if (wingmen.length < 2) {
addWingmen();
msg = "僚机出击!"; color = "#7adcff";
} else {
addScore(200);
msg = "+200 分"; color = "#9fe6c0";
}
sfx.powerup();
break;
case "score":
addScore(500);
msg = "+500 分"; color = "#9fe6c0";
sfx.powerup();
break;
case "life":
player.lives = Math.min(5, player.lives + 1);
msg = "+1 生命"; color = "#69f0ae";
sfx.powerup();
break;
case "bomb":
clearAllEnemies();
msg = "清屏炸弹!"; color = "#7adcff";
sfx.explode();
break;
}
burst(p.x, p.y, color, 16, 200, 3);
addRing(p.x, p.y, color, 56);
addFloatingText(p.x, p.y, msg, color, 17);
if (p.type !== "score" && p.type !== "bomb") showToast(msg, color);
updateHUD();
}
function clearAllEnemies() {
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
addScore(e.worth);
burst(e.x, e.y, "#7adcff", 16, 260, 3);
}
enemies = [];
enemyBullets = [];
shake = 20;
addRing(player.x, player.y, "#7adcff", 160);
sfx.explode();
updateHUD();
}
/* ================= 渲染 ================= */
// 子弹渲染:能量弹 / 菱形激光 / 旋转光轮
function drawBullet(b) {
if (b.kind === "laser") {
// 菱形激光(红橙渐变,细长)
ctx.save();
ctx.translate(b.x, b.y);
const ang = Math.atan2(b.vy || 0, b.vx || 0) + Math.PI / 2;
ctx.rotate(ang);
ctx.shadowColor = "#ff8a50";
ctx.shadowBlur = 12;
const len = b.r * 3.4, wd = b.r * 0.85;
const g = ctx.createLinearGradient(0, -len / 2, 0, len / 2);
g.addColorStop(0, "#ff8a50");
g.addColorStop(0.5, "#ff5252");
g.addColorStop(1, "#ff8a50");
ctx.fillStyle = g;
ctx.beginPath();
ctx.moveTo(0, -len / 2);
ctx.lineTo(wd, 0);
ctx.lineTo(0, len / 2);
ctx.lineTo(-wd, 0);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
} else if (b.kind === "wheel") {
// 旋转光轮(半月形旋转)
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.spin || 0);
ctx.shadowColor = b.color || "#ffd54f";
ctx.shadowBlur = 12;
ctx.fillStyle = b.color || "#ffd54f";
ctx.beginPath();
ctx.arc(0, 0, b.r, 0, Math.PI, false);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
} else {
// 圆形能量弹(蓝白渐变 + 拖尾光晕)
const mid = b.color || "#7adcff";
ctx.save();
ctx.translate(b.x, b.y);
const ang = Math.atan2(b.vy || 0, b.vx || 0);
ctx.rotate(ang);
ctx.shadowColor = mid;
ctx.shadowBlur = 14;
ctx.globalCompositeOperation = "lighter";
// 拖尾
const tg = ctx.createLinearGradient(0, 0, -b.r * 3.5, 0);
tg.addColorStop(0, "rgba(255,255,255,0.85)");
tg.addColorStop(1, "rgba(122,220,255,0)");
ctx.fillStyle = tg;
ctx.beginPath();
ctx.ellipse(0, 0, b.r * 3.5, b.r * 0.8, 0, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = "source-over";
// 核心
const g = ctx.createRadialGradient(0, 0, 0, 0, 0, b.r);
g.addColorStop(0, "#ffffff");
g.addColorStop(0.5, mid);
g.addColorStop(1, "rgba(122,220,255,0)");
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(0, 0, b.r, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawShip(x, y, r, invincible) {
if (invincible && Math.floor(invincible * 12) % 2 === 0) return;
const halo = levelHalo().color;
ctx.save();
ctx.translate(x, y);
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = 0.35 + 0.2 * Math.sin(elapsed * 6);
const aura = ctx.createRadialGradient(0, 0, r * 0.2, 0, 0, r * 2.3);
aura.addColorStop(0, halo);
aura.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = aura;
ctx.beginPath();
ctx.arc(0, 0, r * 2.3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.shadowColor = halo;
ctx.shadowBlur = 16;
ctx.beginPath();
ctx.moveTo(0, -r * 1.6);
ctx.lineTo(r * 0.85, r * 0.45);
ctx.lineTo(0, r * 1.05);
ctx.lineTo(-r * 0.85, r * 0.45);
ctx.closePath();
const g = ctx.createLinearGradient(0, -r * 1.6, 0, r * 1.05);
g.addColorStop(0, PLAYER_BODY_B);
g.addColorStop(1, PLAYER_BODY_A);
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.9)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.ellipse(0, -r * 0.4, r * 0.26, r * 0.5, 0, 0, Math.PI * 2);
ctx.fillStyle = "rgba(255,255,255,0.95)";
ctx.fill();
ctx.save();
ctx.globalCompositeOperation = "lighter";
const fl = r * 0.5 + rand(2, 8);
const fg = ctx.createLinearGradient(0, r * 0.9, 0, r * 0.9 + fl);
fg.addColorStop(0, "#ffffff");
fg.addColorStop(0.4, halo);
fg.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = fg;
ctx.beginPath();
ctx.moveTo(-r * 0.2, r * 0.9);
ctx.lineTo(r * 0.2, r * 0.9);
ctx.lineTo(0, r * 0.9 + fl);
ctx.closePath();
ctx.fill();
ctx.restore();
ctx.restore();
}
function drawWingman(w) {
ctx.save();
ctx.translate(w.x, w.y);
ctx.shadowColor = "#7adcff";
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.moveTo(0, -12);
ctx.lineTo(7, 6);
ctx.lineTo(0, 3);
ctx.lineTo(-7, 6);
ctx.closePath();
const g = ctx.createLinearGradient(0, -12, 0, 6);
g.addColorStop(0, "#00BFFF");
g.addColorStop(1, "#1E90FF");
ctx.fillStyle = g;
ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,0.9)";
ctx.lineWidth = 1;
ctx.stroke();
ctx.restore();
}
function enemyStyle(type) {
if (type === "tank") return { tier: "elite", c1: "#4a1280", c2: "#8A2BE2", glow: "#c9a0ff", core: "#e8d5ff" };
if (type === "zigzag" || type === "transporter") return { tier: "medium", c1: "#8a6a00", c2: "#DAA520", glow: "#ffe08a", core: "#fff3c4" };
return { tier: "small", c1: "#4a0000", c2: "#8B0000", glow: "#ff7a7a", core: "#ffb3b3" };
}
function drawEnemy(e) {
const st = enemyStyle(e.type);
ctx.save();
ctx.translate(e.x, e.y);
if (st.tier === "elite") {
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = 0.45 + 0.25 * Math.sin(elapsed * 5 + e.phase);
ctx.beginPath();
ctx.arc(0, 0, e.r + 9, 0, Math.PI * 2);
ctx.strokeStyle = st.glow;
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.restore();
}
if (st.tier === "medium") {
ctx.save();
ctx.rotate(e.phase * 2);
ctx.strokeStyle = st.glow;
ctx.lineWidth = 1.5;
for (let a = 0; a < 3; a++) {
ctx.rotate(Math.PI * 2 / 3);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(e.r + 7, 0);
ctx.stroke();
ctx.beginPath();
ctx.arc(e.r + 7, 0, 2.6, 0, Math.PI * 2);
ctx.fillStyle = st.glow;
ctx.fill();
}
ctx.restore();
}
if (e.flash > 0) {
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 24;
} else {
ctx.shadowColor = st.glow;
ctx.shadowBlur = 10;
}
if (e.type === "transporter") {
ctx.beginPath();
ctx.roundRect(-e.r, -e.r * 0.5, e.r * 2, e.r * 1.1, 4);
const tg = ctx.createLinearGradient(0, -e.r, 0, e.r);
tg.addColorStop(0, st.c2);
tg.addColorStop(1, st.c1);
ctx.fillStyle = e.flash > 0 ? "#ffffff" : tg;
ctx.fill();
ctx.strokeStyle = st.glow;
ctx.lineWidth = 1;
ctx.stroke();
ctx.fillStyle = st.c2;
ctx.fillRect(-e.r * 0.7, -e.r * 0.2, e.r * 0.5, e.r * 0.5);
ctx.fillRect(0.2 * e.r, -e.r * 0.2, e.r * 0.5, e.r * 0.5);
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(0, 0, e.r * 0.2, 0, Math.PI * 2);
ctx.fillStyle = st.core;
ctx.fill();
ctx.restore();
return;
}
ctx.beginPath();
ctx.moveTo(0, e.r * 1.3);
ctx.lineTo(e.r, -e.r * 0.4);
ctx.lineTo(e.r * 0.5, -e.r * 0.15);
ctx.lineTo(0, -e.r * 0.6);
ctx.lineTo(-e.r * 0.5, -e.r * 0.15);
ctx.lineTo(-e.r, -e.r * 0.4);
ctx.closePath();
const g = ctx.createLinearGradient(0, -e.r, 0, e.r * 1.3);
g.addColorStop(0, st.c2);
g.addColorStop(1, st.c1);
ctx.fillStyle = e.flash > 0 ? "#ffffff" : g;
ctx.fill();
ctx.strokeStyle = st.glow;
ctx.lineWidth = 1;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(0, e.r * 0.15, e.r * 0.26, 0, Math.PI * 2);
ctx.fillStyle = st.core;
ctx.fill();
if (e.type === "kamikaze") {
ctx.strokeStyle = "rgba(255,255,255,0.9)";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(0, 0, e.r + 6 + Math.sin(e.phase * 4) * 2, 0, Math.PI * 2);
ctx.stroke();
}
if (e.type === "tank" && e.hp < e.maxHp) {
const w = e.r * 1.6;
ctx.fillStyle = "rgba(0,0,0,0.5)";
ctx.fillRect(-w / 2, -e.r - 14, w, 5);
ctx.fillStyle = "#c9a0ff";
ctx.fillRect(-w / 2, -e.r - 14, w * (e.hp / e.maxHp), 5);
}
ctx.restore();
}
function drawBoss() {
const b = boss;
ctx.save();
ctx.translate(b.x, b.y);
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.globalAlpha = 0.4 + 0.25 * Math.sin(elapsed * 4);
ctx.beginPath();
ctx.arc(0, 0, b.r + 18, 0, Math.PI * 2);
ctx.strokeStyle = "#c9a0ff";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
ctx.shadowColor = b.flash > 0 ? "#ffffff" : "#8A2BE2";
ctx.shadowBlur = b.flash > 0 ? 30 : 22;
ctx.beginPath();
ctx.moveTo(0, -b.r);
ctx.lineTo(b.r, -b.r * 0.3);
ctx.lineTo(b.r * 0.6, b.r * 0.2);
ctx.lineTo(b.r * 0.9, b.r * 0.7);
ctx.lineTo(0, b.r);
ctx.lineTo(-b.r * 0.9, b.r * 0.7);
ctx.lineTo(-b.r * 0.6, b.r * 0.2);
ctx.lineTo(-b.r, -b.r * 0.3);
ctx.closePath();
const g = ctx.createLinearGradient(0, -b.r, 0, b.r);
g.addColorStop(0, "#c9a0ff");
g.addColorStop(0.5, "#8A2BE2");
g.addColorStop(1, "#3d0a5e");
ctx.fillStyle = b.flash > 0 ? "#ffffff" : g;
ctx.fill();
ctx.strokeStyle = "#e8d5ff";
ctx.lineWidth = 1;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.arc(0, 0, b.r * 0.36, 0, Math.PI * 2);
ctx.fillStyle = "#ffd54f";
ctx.fill();
ctx.beginPath();
ctx.arc(0, 0, b.r * 0.18, 0, Math.PI * 2);
ctx.fillStyle = "#ffffff";
ctx.fill();
ctx.restore();
}
function drawLaser() {
if (boss.laserState === "charging") {
const on = Math.floor(elapsed * 12) % 2 === 0;
ctx.save();
ctx.globalAlpha = on ? 0.85 : 0.3;
ctx.strokeStyle = "#ff5252";
ctx.lineWidth = 3;
ctx.setLineDash([10, 8]);
ctx.beginPath();
ctx.moveTo(boss.laserX, boss.y + boss.r * 0.4);
ctx.lineTo(boss.laserX, H);
ctx.stroke();
ctx.restore();
} else if (boss.laserState === "firing") {
const bw = 26;
ctx.save();
ctx.globalCompositeOperation = "lighter";
const g = ctx.createLinearGradient(boss.laserX - bw, 0, boss.laserX + bw, 0);
g.addColorStop(0, "rgba(255,60,60,0)");
g.addColorStop(0.5, "#ffffff");
g.addColorStop(1, "rgba(255,60,60,0)");
ctx.fillStyle = g;
ctx.fillRect(boss.laserX - bw, boss.y - 10, bw * 2, H - boss.y + 10);
ctx.shadowColor = "#ff5252";
ctx.shadowBlur = 22;
ctx.fillStyle = "rgba(255,82,82,0.55)";
ctx.fillRect(boss.laserX - 7, boss.y - 10, 14, H - boss.y + 10);
ctx.restore();
}
}
function drawBossAlert() {
const extra = bossAlertTimer > 0 ? bossAlertTimer : 0;
const base = 0.16 + 0.12 * Math.sin(elapsed * 8);
const intensity = clamp(base + extra * 0.35, 0, 0.7);
const g = ctx.createRadialGradient(W / 2, H / 2, Math.min(W, H) * 0.35, W / 2, H / 2, Math.max(W, H) * 0.72);
g.addColorStop(0, "rgba(255,0,0,0)");
g.addColorStop(1, "rgba(255,30,30," + intensity + ")");
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
}
function drawBossBar() {
const bw = Math.min(W * 0.6, 420);
const x = (W - bw) / 2, y = 92;
const segments = 2; // 两段血条(50% 为界)
const segW = bw / segments;
const hpPer = boss.maxHp / segments;
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(x - 4, y - 4, bw + 8, 20);
for (let s = 0; s < segments; s++) {
const frac = clamp((boss.hp - boss.maxHp + (s + 1) * hpPer) / hpPer, 0, 1);
ctx.fillStyle = "rgba(255,255,255,0.12)";
ctx.fillRect(x + s * segW, y, segW - 2, 12);
if (frac > 0) {
const g = ctx.createLinearGradient(0, y, 0, y + 12);
if (s === 0) {
g.addColorStop(0, "#c9a0ff");
g.addColorStop(1, "#8A2BE2");
} else {
g.addColorStop(0, "#ff8a80");
g.addColorStop(1, "#c62828");
}
ctx.fillStyle = g;
ctx.fillRect(x + s * segW, y, (segW - 2) * frac, 12);
}
}
ctx.fillStyle = boss.phase === 2 ? "#ff5252" : "#ffffff";
ctx.font = "bold 13px 'Segoe UI', 'Microsoft YaHei', sans-serif";
ctx.textAlign = "center";
ctx.fillText(boss.phase === 2 ? "BOSS · PHASE 2" : "BOSS", W / 2, y - 10);
}
function drawPowerup(p) {
const bob = Math.sin(p.phase) * 3;
ctx.save();
ctx.translate(p.x, p.y + bob);
const colors = {
upgrade: "#ffd54f", downgrade: "#ff8a80", wingman: "#7adcff",
life: "#69f0ae", score: "#9fe6c0", bomb: "#7adcff"
};
const c = colors[p.type];
ctx.shadowColor = c;
ctx.shadowBlur = 18;
ctx.beginPath();
ctx.arc(0, 0, p.r, 0, Math.PI * 2);
ctx.fillStyle = "rgba(10,18,40,0.85)";
ctx.fill();
ctx.strokeStyle = c;
ctx.lineWidth = 2;
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = c;
ctx.font = "bold 15px 'Segoe UI', sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const label = { upgrade: "S", downgrade: "D", wingman: "W", life: "♥", score: "+", bomb: "✹" }[p.type];
ctx.fillText(label, 0, 1);
ctx.restore();
}
function drawBackground() {
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, "#050818");
bg.addColorStop(0.5, "#0a1030");
bg.addColorStop(1, "#03040a");
ctx.fillStyle = bg;
ctx.fillRect(0, 0, W, H);
ctx.save();
ctx.globalCompositeOperation = "lighter";
const neb = ctx.createRadialGradient(W * 0.3, H * 0.25, 0, W * 0.3, H * 0.25, 300);
neb.addColorStop(0, "rgba(47,90,180,0.14)");
neb.addColorStop(1, "rgba(47,90,180,0)");
ctx.fillStyle = neb;
ctx.fillRect(0, 0, W, H);
const neb2 = ctx.createRadialGradient(W * 0.8, H * 0.6, 0, W * 0.8, H * 0.6, 340);
neb2.addColorStop(0, "rgba(120,40,160,0.12)");
neb2.addColorStop(1, "rgba(120,40,160,0)");
ctx.fillStyle = neb2;
ctx.fillRect(0, 0, W, H);
ctx.restore();
for (const s of stars) {
const a = 0.5 + 0.5 * Math.sin(s.tw);
ctx.globalAlpha = a;
ctx.fillStyle = "#cfe6ff";
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function render() {
ctx.clearRect(0, 0, W, H);
ctx.save();
if (shake > 0) {
ctx.translate(rand(-shake, shake) * 0.5, rand(-shake, shake) * 0.5);
}
drawBackground();
for (const p of powerups) drawPowerup(p);
for (const b of enemyBullets) drawBullet(b);
for (const b of bullets) drawBullet(b);
for (const e of enemies) drawEnemy(e);
if (boss) drawBoss();
if (boss) drawLaser();
for (const w of wingmen) drawWingman(w);
if (player.alive) {
drawShip(player.x, player.y, player.r, player.invincible > 0);
if (player.invincible > 0) {
ctx.save();
ctx.globalCompositeOperation = "lighter";
ctx.beginPath();
ctx.arc(player.x, player.y, player.r + 10, 0, Math.PI * 2);
ctx.strokeStyle = "rgba(122,220,255," + (0.4 + 0.3 * Math.sin(elapsed * 20)) + ")";
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
}
}
ctx.save();
ctx.globalCompositeOperation = "lighter";
for (const p of particles) {
ctx.globalAlpha = Math.max(0, p.life / p.maxLife);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
ctx.globalAlpha = 1;
for (const r of rings) {
ctx.save();
ctx.globalAlpha = Math.max(0, r.life / r.maxLife);
ctx.strokeStyle = r.color;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(r.x, r.y, r.r, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
for (const f of floatingTexts) {
ctx.save();
ctx.globalAlpha = Math.max(0, f.life / f.maxLife);
ctx.fillStyle = f.color;
ctx.font = "bold " + f.size + "px 'Segoe UI', 'Microsoft YaHei', sans-serif";
ctx.textAlign = "center";
ctx.shadowColor = f.color;
ctx.shadowBlur = 8;
ctx.fillText(f.text, f.x, f.y);
ctx.restore();
}
ctx.restore();
if (boss && boss.phase === 2) drawBossAlert();
if (boss) drawBossBar();
if (comboPopupData) {
const cp = comboPopupData;
const prog = 1 - cp.life / cp.maxLife;
const scale = 0.5 + Math.min(1, prog * 5) * 0.7;
const alpha = cp.life > 0.6 ? 1 : (cp.life / 0.6);
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(W / 2, H * 0.35);
ctx.scale(scale, scale);
ctx.fillStyle = "#ffd54f";
ctx.font = "bold 56px 'Segoe UI', 'Microsoft YaHei', sans-serif";
ctx.textAlign = "center";
ctx.shadowColor = "#ffd54f";
ctx.shadowBlur = 30;
ctx.fillText(cp.text, 0, 0);
ctx.restore();
}
if (phase2Popup) {
const pp = phase2Popup;
const prog = 1 - pp.life / pp.maxLife;
const scale = 1 + Math.min(1, prog * 4) * 0.6;
const alpha = pp.life > 1.6 ? 1 : (pp.life / 1.6);
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(W / 2, H * 0.4);
ctx.scale(scale, scale);
ctx.fillStyle = "#ff5252";
ctx.font = "bold 72px 'Segoe UI', 'Microsoft YaHei', sans-serif";
ctx.textAlign = "center";
ctx.shadowColor = "#ff5252";
ctx.shadowBlur = 40;
ctx.fillText("PHASE 2", 0, 0);
ctx.restore();
}
}
/* ================= 主循环 ================= */
let last = performance.now();
let fpsAccum = 0, fpsFrames = 0, fps = 60;
function loop(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05;
// FPS 统计 + 自适应粒子密度(卡顿自动降低发射量)
fpsAccum += dt;
fpsFrames++;
if (fpsAccum >= 0.5) {
fps = fpsFrames / fpsAccum;
fpsAccum = 0;
fpsFrames = 0;
if (fps < 45) particleScale = Math.max(0.3, particleScale - 0.15);
else if (fps > 55) particleScale = Math.min(1, particleScale + 0.05);
}
if (phase2Popup) {
phase2Popup.life -= dt;
if (phase2Popup.life <= 0) phase2Popup = null;
}
if (freezeTimer > 0) {
freezeTimer -= dt; // 顿帧:冻结世界,仍渲染
} else if (!paused) {
update(dt);
}
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
updateHUD();
backToMenu();
})();
</script>
</body>
</html>
Game Source: 星际雷霆 · 竖版卷轴飞行射击
Creator: SuperOwl71
Libraries: none
Complexity: complex (2245 lines, 66.0 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-superowl71" to link back to the original. Then publish at arcadelab.ai/publish.