疾风跑酷 · 自动横屏
by LaserPirate331213 lines34.2 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, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#05071a">
<title>疾风跑酷 · 自动横屏</title>
<style>
:root {
--sat: env(safe-area-inset-top, 0px);
--sar: env(safe-area-inset-right, 0px);
--sab: env(safe-area-inset-bottom, 0px);
--sal: env(safe-area-inset-left, 0px);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
background: #05071a;
overflow: hidden;
touch-action: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif;
}
/* 舞台:无论手机横竖,游戏画面始终保持横屏,靠旋转适配 */
#stage {
position: fixed;
left: 50%;
top: 50%;
transform-origin: 50% 50%;
transform: translate(-50%, -50%) rotate(0deg);
will-change: transform;
}
canvas {
display: block;
border-radius: 14px;
background: #080b1c;
box-shadow: 0 24px 80px rgba(0, 0, 0, .8), 0 0 0 1px rgba(255, 255, 255, .06);
}
</style>
</head>
<body>
<div id="stage"><canvas id="game"></canvas></div>
<script>
(function () {
'use strict';
// ================= 画布与舞台 =================
const W = 960, H = 540; // 游戏内部逻辑分辨率(横屏)
const canvas = document.getElementById('game');
const stage = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const DPR = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(W * DPR);
canvas.height = Math.round(H * DPR);
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
let rotatedDeg = 0; // 0 = 正常横屏;90 = 竖屏时画面侧转
let scale = 1; // 逻辑像素 -> CSS 像素
let stageW = W, stageH = H;
let stageCenterX = window.innerWidth / 2;
let stageCenterY = window.innerHeight / 2;
function readSafeInsets() {
const cs = getComputedStyle(document.documentElement);
const num = function (name) {
const v = parseFloat(cs.getPropertyValue(name));
return isNaN(v) ? 0 : v;
};
return { t: num('--sat'), r: num('--sar'), b: num('--sab'), l: num('--sal') };
}
// 核心:窗口是竖的 -> 画面旋转 90°;窗口是横的 -> 正常横屏
function fit() {
const vw = window.innerWidth;
const vh = window.innerHeight;
const ins = readSafeInsets();
const availW = Math.max(120, vw - ins.l - ins.r);
const availH = Math.max(120, vh - ins.t - ins.b);
// 安全区中心,避免刘海遮挡内容
stageCenterX = (ins.l + (vw - ins.r)) / 2;
stageCenterY = (ins.t + (vh - ins.b)) / 2;
const wantRotate = vh > vw ? 90 : 0; // 竖屏就侧转
let s;
if (wantRotate === 90) {
// 旋转后:视觉宽度 = stageH,视觉高度 = stageW
s = Math.min(availW / H, availH / W);
} else {
s = Math.min(availW / W, availH / H);
}
s = Math.max(0.05, s);
rotatedDeg = wantRotate;
scale = s;
stageW = Math.round(W * s);
stageH = Math.round(H * s);
stage.style.left = stageCenterX + 'px';
stage.style.top = stageCenterY + 'px';
stage.style.width = stageW + 'px';
stage.style.height = stageH + 'px';
stage.style.transform = 'translate(-50%, -50%) rotate(' + rotatedDeg + 'deg)';
canvas.style.width = stageW + 'px';
canvas.style.height = stageH + 'px';
}
// 屏幕坐标 -> 游戏逻辑坐标(考虑旋转与缩放)
function getPos(e) {
const dx = e.clientX - stageCenterX;
const dy = e.clientY - stageCenterY;
const rad = -rotatedDeg * Math.PI / 180;
const cos = Math.cos(rad), sin = Math.sin(rad);
const rx = dx * cos - dy * sin;
const ry = dx * sin + dy * cos;
return {
x: rx / scale + W / 2,
y: ry / scale + H / 2
};
}
let fitTimer = null;
function scheduleFit() {
fit();
clearTimeout(fitTimer);
fitTimer = setTimeout(fit, 240); // 应对微信里 innerHeight 延迟更新
setTimeout(fit, 620);
}
fit();
window.addEventListener('resize', scheduleFit);
window.addEventListener('orientationchange', scheduleFit);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', scheduleFit);
}
// ================= 常量 =================
const GROUND_Y = 440;
const GRAVITY = 2600;
const JUMP_V = -920;
const JUMP2_V = -780;
const PW = 32, PH = 48; // 站立碰撞盒
const SW = 50, SH = 28; // 滑行碰撞盒
const PLAYER_X = 160;
const BULLET_SPEED = 950;
const SKILL_CD = 0.20;
const BTN = {
skill: { x: 102, y: 412, r: 52, color: '#b06bff', glow: '#8a2be2' },
jump: { x: 858, y: 372, r: 56, color: '#4de2ff', glow: '#00a8ff' },
slide: { x: 738, y: 466, r: 42, color: '#ffb347', glow: '#ff8c00' }
};
// ================= 状态 =================
const state = {
mode: 'menu', // menu | playing | over
time: 0,
meters: 0,
speed: 300,
scroll: 0,
spawnTimer: 1.6,
bulletTimer: 2.2,
skillCd: 0,
kills: 0,
score: 0,
best: 0,
shake: 0,
overTimer: 0
};
try { state.best = Number(localStorage.getItem('runnerBestLandscape') || 0) || 0; } catch (e) {}
const player = {
x: PLAYER_X,
y: GROUND_Y,
vy: 0,
jumps: 0,
sliding: false,
runPhase: 0
};
let obstacles = [];
let enemyBullets = [];
let playerBullets = [];
let particles = [];
const clouds = [];
const stars = [];
for (let i = 0; i < 7; i++) {
clouds.push({
x: Math.random() * W,
y: 40 + Math.random() * 200,
s: 0.6 + Math.random() * 0.9,
sp: 0.08 + Math.random() * 0.12
});
}
for (let i = 0; i < 90; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * 380,
r: Math.random() * 1.3 + 0.3,
p: Math.random() * Math.PI * 2
});
}
// ================= 输入状态 =================
const pressed = { skill: false, jump: false, slide: false };
const slidePointers = new Set();
// ================= 工具 =================
function hash(n) {
const s = Math.sin(n * 127.1) * 43758.5453;
return s - Math.floor(s);
}
function roundRect(c, x, y, w, h, r) {
r = Math.max(0, Math.min(r, Math.abs(w) / 2, Math.abs(h) / 2));
c.beginPath();
c.moveTo(x + r, y);
c.arcTo(x + w, y, x + w, y + h, r);
c.arcTo(x + w, y + h, x, y + h, r);
c.arcTo(x, y + h, x, y, r);
c.arcTo(x, y, x + w, y, r);
c.closePath();
}
function difficulty() {
return Math.min(state.meters / 1800, 1);
}
function onGround() {
return player.y >= GROUND_Y - 0.5;
}
function playerRect() {
if (player.sliding && onGround()) {
return { x: player.x - SW / 2, y: player.y - SH, w: SW, h: SH };
}
return { x: player.x - PW / 2, y: player.y - PH, w: PW, h: PH };
}
function overlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
}
function circleRect(cx, cy, cr, r) {
const nx = Math.max(r.x, Math.min(cx, r.x + r.w));
const ny = Math.max(r.y, Math.min(cy, r.y + r.h));
const dx = cx - nx, dy = cy - ny;
return dx * dx + dy * dy < cr * cr;
}
function inCircle(p, c) {
const dx = p.x - c.x, dy = p.y - c.y;
return dx * dx + dy * dy < c.r * c.r;
}
// ================= 粒子 =================
function spawnParticles(x, y, n, color, spread) {
spread = spread || 240;
for (let i = 0; i < n; i++) {
if (particles.length > 340) particles.shift();
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5) * spread,
vy: (Math.random() - 0.5) * spread,
life: 0.30 + Math.random() * 0.40,
max: 0.70,
r: 1.5 + Math.random() * 3,
color: color
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { particles.splice(i, 1); continue; }
p.vy += 760 * dt;
p.x += p.vx * dt;
p.y += p.vy * dt;
if (p.y > GROUND_Y + 4) {
p.y = GROUND_Y + 4;
p.vy *= -0.35;
p.vx *= 0.7;
}
}
}
// ================= 游戏流程 =================
function restart() {
state.mode = 'playing';
state.meters = 0;
state.speed = 300;
state.scroll = 0;
state.spawnTimer = 1.5;
state.bulletTimer = 1.8;
state.skillCd = 0;
state.kills = 0;
state.score = 0;
state.shake = 0;
state.overTimer = 0;
player.y = GROUND_Y;
player.vy = 0;
player.jumps = 0;
player.sliding = false;
player.runPhase = 0;
obstacles = [];
enemyBullets = [];
playerBullets = [];
particles = [];
slidePointers.clear();
pressed.skill = pressed.jump = pressed.slide = false;
}
function gameOver() {
if (state.mode !== 'playing') return;
state.mode = 'over';
state.overTimer = 0;
state.shake = 16;
spawnParticles(player.x, player.y - 24, 30, '#ff5ea8', 420);
const total = Math.floor(state.meters) + state.score;
if (total > state.best) {
state.best = total;
try { localStorage.setItem('runnerBestLandscape', String(total)); } catch (e) {}
}
}
function doJump() {
if (state.mode !== 'playing') return;
if (player.jumps >= 2) return;
player.vy = player.jumps === 0 ? JUMP_V : JUMP2_V;
player.jumps++;
spawnParticles(player.x, player.y, 8, player.jumps === 1 ? '#6ef0ff' : '#ff5ea8', 200);
}
function fireSkill() {
if (state.mode !== 'playing') return;
if (state.skillCd > 0) return;
state.skillCd = SKILL_CD;
const sx = player.x + 22;
const sy = player.y - 28;
let target = null;
let bestDx = Infinity;
for (let i = 0; i < enemyBullets.length; i++) {
const b = enemyBullets[i];
const dx = b.x - sx;
if (dx < 0) continue;
if (dx < bestDx && dx < 760) {
bestDx = dx;
target = b;
}
}
let vx = BULLET_SPEED, vy = 0;
if (target) {
const dx = target.x - sx;
const dy = target.y - sy;
const len = Math.hypot(dx, dy) || 1;
vx = dx / len * BULLET_SPEED;
vy = dy / len * BULLET_SPEED;
}
playerBullets.push({
x: sx, y: sy,
vx: vx, vy: vy,
r: 7,
life: 1.35
});
spawnParticles(sx, sy, 6, '#6ef0ff', 180);
}
// ================= 生成障碍 =================
function spawnObstacle() {
const d = difficulty();
const x = W + 60;
const pool = [
{ k: 'low', w: 3 },
{ k: 'overhead', w: d > 0.10 ? 2.4 : 0 },
{ k: 'double', w: d > 0.42 ? 1.4 + d * 1.6 : 0 }
];
let total = 0;
for (let i = 0; i < pool.length; i++) total += pool[i].w;
let pick = Math.random() * total;
let kind = 'low';
for (let i = 0; i < pool.length; i++) {
if (pick < pool[i].w) { kind = pool[i].k; break; }
pick -= pool[i].w;
}
if (kind === 'low') {
const h = 34 + Math.random() * 10;
obstacles.push({
x: x, y: GROUND_Y - h,
w: 30 + Math.random() * 10, h: h,
kind: 'low'
});
} else if (kind === 'overhead') {
obstacles.push({
x: x, y: GROUND_Y - 32 - 160,
w: 62, h: 160,
kind: 'overhead'
});
} else {
obstacles.push({ x: x, y: GROUND_Y - 38, w: 28, h: 38, kind: 'low' });
obstacles.push({ x: x + 84 + Math.random() * 26, y: GROUND_Y - 38, w: 28, h: 38, kind: 'low' });
}
}
// ================= 生成弹幕 =================
function spawnEnemyBullet() {
const d = difficulty();
const y = 190 + Math.random() * 220;
const speed = 300 + d * 280;
const ang = (Math.random() - 0.5) * 0.22;
enemyBullets.push({
x: W + 40,
y: y,
vx: -Math.cos(ang) * speed,
vy: Math.sin(ang) * speed,
r: 11,
phase: Math.random() * Math.PI * 2
});
}
// ================= 单步逻辑 =================
function step(dt) {
const d = difficulty();
state.speed = 300 + d * 520;
state.meters += state.speed * dt / 10;
state.scroll += state.speed * dt;
if (state.skillCd > 0) {
state.skillCd = Math.max(0, state.skillCd - dt);
}
// --- 玩家物理 ---
player.vy += GRAVITY * dt;
if (player.sliding && !onGround()) {
player.vy += GRAVITY * 1.25 * dt; // 空中按住滑行快速下坠
}
player.y += player.vy * dt;
if (player.y >= GROUND_Y) {
if (player.vy > 520) {
spawnParticles(player.x, GROUND_Y, 6, '#6ef0ff', 160);
}
player.y = GROUND_Y;
player.vy = 0;
player.jumps = 0;
}
player.runPhase += dt * (state.speed / 55);
if (onGround() && Math.random() < dt * 26) {
if (particles.length > 340) particles.shift();
particles.push({
x: player.x - 12,
y: GROUND_Y - 2,
vx: -state.speed * 0.16 - Math.random() * 40,
vy: -Math.random() * 70,
life: 0.38, max: 0.38,
r: 1.4 + Math.random() * 2.2,
color: '#4de2ff'
});
}
// --- 生成障碍 ---
state.spawnTimer -= dt;
if (state.spawnTimer <= 0) {
spawnObstacle();
const gap = (1.55 - d * 0.90) * (0.82 + Math.random() * 0.42);
state.spawnTimer = Math.max(0.48, gap);
}
// --- 生成弹幕 ---
state.bulletTimer -= dt;
if (state.bulletTimer <= 0) {
spawnEnemyBullet();
const gap = (2.45 - d * 1.70) * (0.75 + Math.random() * 0.55);
state.bulletTimer = Math.max(0.55, gap);
}
// --- 移动障碍 ---
for (let i = obstacles.length - 1; i >= 0; i--) {
const o = obstacles[i];
o.x -= state.speed * dt;
if (o.x + o.w < -80) obstacles.splice(i, 1);
}
// --- 移动敌方弹幕 ---
for (let i = enemyBullets.length - 1; i >= 0; i--) {
const b = enemyBullets[i];
b.x += b.vx * dt;
b.y += b.vy * dt;
if (b.x + b.r < -50 || b.y < -60 || b.y > H + 60) {
enemyBullets.splice(i, 1);
}
}
// --- 移动玩家子弹 ---
for (let i = playerBullets.length - 1; i >= 0; i--) {
const b = playerBullets[i];
b.x += b.vx * dt;
b.y += b.vy * dt;
b.life -= dt;
if (b.life <= 0 || b.x > W + 50 || b.y < -50 || b.y > H + 50) {
playerBullets.splice(i, 1);
}
}
// --- 子弹 vs 弹幕 ---
for (let i = playerBullets.length - 1; i >= 0; i--) {
const pb = playerBullets[i];
let hit = false;
for (let j = enemyBullets.length - 1; j >= 0; j--) {
const eb = enemyBullets[j];
const dx = pb.x - eb.x;
const dy = pb.y - eb.y;
if (dx * dx + dy * dy < 30 * 30) {
spawnParticles(eb.x, eb.y, 16, '#ff5ea8', 320);
spawnParticles(eb.x, eb.y, 6, '#ffffff', 160);
enemyBullets.splice(j, 1);
state.kills++;
state.score += 50;
hit = true;
break;
}
}
if (hit) {
playerBullets.splice(i, 1);
}
}
// --- 玩家 vs 障碍 ---
const pr = playerRect();
for (let i = 0; i < obstacles.length; i++) {
if (overlap(pr, obstacles[i])) {
gameOver();
return;
}
}
// --- 玩家 vs 弹幕 ---
for (let i = 0; i < enemyBullets.length; i++) {
const b = enemyBullets[i];
if (circleRect(b.x, b.y, b.r * 0.85, pr)) {
gameOver();
return;
}
}
}
// ================= 主更新 =================
function update(dt) {
state.time += dt;
for (let i = 0; i < clouds.length; i++) {
const c = clouds[i];
const spd = state.mode === 'playing' ? state.speed : 90;
c.x -= (c.sp * spd * 0.32 + 10) * dt;
if (c.x < -160) {
c.x = W + 160;
c.y = 40 + Math.random() * 200;
c.s = 0.6 + Math.random() * 0.9;
}
}
if (state.shake > 0) {
state.shake *= Math.pow(0.0008, dt);
if (state.shake < 0.4) state.shake = 0;
}
updateParticles(dt);
if (state.mode === 'over') state.overTimer += dt;
if (state.mode !== 'playing') return;
const steps = Math.max(1, Math.ceil(dt / 0.016));
const sub = dt / steps;
for (let i = 0; i < steps; i++) {
if (state.mode !== 'playing') break;
step(sub);
}
}
// ================= 绘制:背景 =================
function drawSky() {
const d = difficulty();
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, 'hsl(' + (228 + d * 42) + ', 60%, 8%)');
g.addColorStop(0.55, 'hsl(' + (250 + d * 55) + ', 55%, 17%)');
g.addColorStop(1, 'hsl(' + (276 + d * 62) + ', 50%, 27%)');
ctx.fillStyle = g;
ctx.fillRect(0, 0, W, H);
}
function drawStars() {
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
const a = 0.22 + 0.78 * Math.abs(Math.sin(state.time * 1.4 + s.p));
ctx.globalAlpha = a;
ctx.fillStyle = '#cfe8ff';
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
function drawClouds() {
ctx.fillStyle = 'rgba(150, 190, 255, .065)';
for (let i = 0; i < clouds.length; i++) {
const c = clouds[i];
ctx.beginPath();
ctx.ellipse(c.x, c.y, 58 * c.s, 18 * c.s, 0, 0, Math.PI * 2);
ctx.ellipse(c.x + 38 * c.s, c.y + 6 * c.s, 36 * c.s, 13 * c.s, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawSkyline(parallax, color, baseY, maxH, period) {
const scroll = state.scroll * parallax;
const startIdx = Math.floor(scroll / period);
const offset = scroll - startIdx * period;
for (let i = -1; i <= Math.ceil(W / period) + 1; i++) {
const idx = startIdx + i;
const x = i * period - offset;
const h = 42 + hash(idx) * maxH;
ctx.fillStyle = color;
ctx.fillRect(x, baseY - h, period - 5, h);
const rows = Math.floor(h / 20);
for (let r = 1; r < rows; r++) {
for (let c = 0; c < 2; c++) {
if (hash(idx * 31 + r * 7 + c * 13) > 0.58) {
ctx.fillStyle = 'rgba(120, 200, 255, .13)';
ctx.fillRect(x + 7 + c * 15, baseY - h + r * 20, 4, 7);
}
}
}
}
}
function drawGround() {
ctx.fillStyle = '#070a1a';
ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y);
ctx.save();
ctx.shadowBlur = 18;
ctx.shadowColor = '#4de2ff';
ctx.strokeStyle = '#4de2ff';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(0, GROUND_Y);
ctx.lineTo(W, GROUND_Y);
ctx.stroke();
ctx.restore();
const off = state.scroll % 56;
ctx.strokeStyle = 'rgba(77, 226, 255, .11)';
ctx.lineWidth = 3;
for (let x = -off; x < W + 56; x += 56) {
ctx.beginPath();
ctx.moveTo(x, GROUND_Y + 26);
ctx.lineTo(x + 26, GROUND_Y + 26);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x + 26, GROUND_Y + 52);
ctx.lineTo(x + 46, GROUND_Y + 52);
ctx.stroke();
}
}
// ================= 绘制:障碍 =================
function drawObstacles() {
for (let i = 0; i < obstacles.length; i++) {
const o = obstacles[i];
let color = '#ff4d6d', glow = '#ff2d55';
if (o.kind === 'overhead') {
color = '#ff9f43';
glow = '#ff7f00';
}
ctx.save();
ctx.shadowBlur = 18;
ctx.shadowColor = glow;
ctx.fillStyle = color;
roundRect(ctx, o.x, o.y, o.w, o.h, 6);
ctx.fill();
ctx.restore();
ctx.fillStyle = 'rgba(255, 255, 255, .24)';
roundRect(ctx, o.x + 4, o.y + 4, Math.min(o.w - 8, 7), o.h - 8, 3);
ctx.fill();
if (o.kind === 'overhead') {
ctx.save();
ctx.shadowBlur = 16;
ctx.shadowColor = glow;
ctx.fillStyle = 'rgba(255, 200, 120, .75)';
roundRect(ctx, o.x - 6, o.y - 6, o.w + 12, 10, 4);
ctx.fill();
ctx.restore();
}
}
}
// ================= 绘制:弹幕与子弹 =================
function drawEnemyBullets() {
for (let i = 0; i < enemyBullets.length; i++) {
const b = enemyBullets[i];
const pulse = 1 + Math.sin(state.time * 10 + b.phase) * 0.10;
ctx.save();
ctx.shadowBlur = 22;
ctx.shadowColor = '#ff2d9e';
const g = ctx.createRadialGradient(b.x, b.y, 1, b.x, b.y, b.r * 2.3 * pulse);
g.addColorStop(0, '#ffe0f2');
g.addColorStop(0.35, '#ff5ea8');
g.addColorStop(1, 'rgba(170, 20, 95, 0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r * 2.3 * pulse, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#ffffff';
ctx.beginPath();
ctx.arc(b.x, b.y, b.r * 0.42, 0, Math.PI * 2);
ctx.fill();
}
}
function drawPlayerBullets() {
for (let i = 0; i < playerBullets.length; i++) {
const b = playerBullets[i];
ctx.save();
ctx.globalAlpha = 0.55;
ctx.strokeStyle = '#6ef0ff';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.shadowBlur = 14;
ctx.shadowColor = '#00c8ff';
ctx.beginPath();
ctx.moveTo(b.x, b.y);
ctx.lineTo(b.x - b.vx * 0.028, b.y - b.vy * 0.028);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.shadowBlur = 18;
ctx.shadowColor = '#00e5ff';
const g = ctx.createRadialGradient(b.x, b.y, 1, b.x, b.y, b.r * 2);
g.addColorStop(0, '#ffffff');
g.addColorStop(0.45, '#6ef0ff');
g.addColorStop(1, 'rgba(0, 200, 255, 0)');
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(b.x, b.y, b.r * 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
// ================= 绘制:玩家 =================
function drawPlayer() {
const r = playerRect();
const cx = player.x;
const airH = GROUND_Y - player.y;
const shScale = Math.max(0, 1 - airH / 220);
if (shScale > 0.02) {
ctx.globalAlpha = 0.30 * shScale;
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.ellipse(player.x, GROUND_Y + 6, 26 * shScale + 4, 6 * shScale + 2, 0, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
ctx.save();
ctx.strokeStyle = '#ff5ea8';
ctx.lineWidth = 5;
ctx.lineCap = 'round';
ctx.shadowBlur = 14;
ctx.shadowColor = '#ff5ea8';
ctx.beginPath();
const sy = r.y + r.h * 0.26;
ctx.moveTo(r.x + 4, sy);
ctx.quadraticCurveTo(
r.x - 18, sy + Math.sin(state.time * 18) * 8,
r.x - 36, sy + 8 + Math.sin(state.time * 18 + 1) * 10
);
ctx.stroke();
ctx.restore();
ctx.save();
ctx.shadowBlur = 22;
ctx.shadowColor = '#4de2ff';
const g = ctx.createLinearGradient(r.x, r.y, r.x, r.y + r.h);
g.addColorStop(0, '#9df6ff');
g.addColorStop(1, '#3ac9ff');
ctx.fillStyle = g;
roundRect(ctx, r.x, r.y, r.w, r.h, 8);
ctx.fill();
ctx.restore();
ctx.fillStyle = '#061426';
const eyeY = r.y + r.h * 0.36;
const eyeR = (player.sliding && onGround()) ? 2.6 : 3.4;
ctx.beginPath();
ctx.arc(cx - r.w * 0.20, eyeY, eyeR, 0, Math.PI * 2);
ctx.arc(cx + r.w * 0.20, eyeY, eyeR, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 255, 255, .9)';
ctx.beginPath();
ctx.arc(cx - r.w * 0.20 - 1, eyeY - 1, 1.2, 0, Math.PI * 2);
ctx.arc(cx + r.w * 0.20 - 1, eyeY - 1, 1.2, 0, Math.PI * 2);
ctx.fill();
}
// ================= 绘制:粒子 =================
function drawParticles() {
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
ctx.globalAlpha = Math.max(0, p.life / p.max);
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ================= 绘制:按钮 =================
function drawIconSkill() {
ctx.beginPath();
ctx.moveTo(-3, -20);
ctx.lineTo(-15, 3);
ctx.lineTo(-2, 3);
ctx.lineTo(4, 21);
ctx.lineTo(15, -3);
ctx.lineTo(2, -3);
ctx.closePath();
ctx.fill();
}
function drawIconJump() {
ctx.beginPath();
ctx.moveTo(0, -21);
ctx.lineTo(-17, 2);
ctx.lineTo(-7, 2);
ctx.lineTo(-7, 21);
ctx.lineTo(7, 21);
ctx.lineTo(7, 2);
ctx.lineTo(17, 2);
ctx.closePath();
ctx.fill();
}
function drawIconSlide() {
ctx.beginPath();
ctx.moveTo(0, 19);
ctx.lineTo(-15, -2);
ctx.lineTo(-5, -2);
ctx.lineTo(-5, -19);
ctx.lineTo(5, -19);
ctx.lineTo(5, -2);
ctx.lineTo(15, -2);
ctx.closePath();
ctx.fill();
}
function drawButton(btn, active, iconFn, cooldownRatio) {
const x = btn.x, y = btn.y, r = btn.r;
ctx.save();
ctx.globalAlpha = active ? 0.95 : 0.55;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(8, 12, 28, .58)';
ctx.fill();
ctx.lineWidth = active ? 4 : 3;
ctx.strokeStyle = btn.color;
ctx.shadowBlur = active ? 26 : 12;
ctx.shadowColor = btn.glow;
ctx.stroke();
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.fillStyle = btn.color;
ctx.strokeStyle = btn.color;
ctx.lineWidth = 3.5;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowBlur = active ? 18 : 10;
ctx.shadowColor = btn.glow;
iconFn();
ctx.restore();
if (cooldownRatio > 0) {
ctx.beginPath();
ctx.moveTo(x, y);
ctx.arc(x, y, r - 1, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * cooldownRatio, false);
ctx.closePath();
ctx.fillStyle = 'rgba(5, 8, 20, .60)';
ctx.fill();
}
}
function drawButtons() {
if (state.mode !== 'playing') return;
const cd = state.skillCd > 0 ? state.skillCd / SKILL_CD : 0;
drawButton(BTN.skill, pressed.skill, drawIconSkill, cd);
drawButton(BTN.jump, pressed.jump, drawIconJump, 0);
drawButton(BTN.slide, pressed.slide, drawIconSlide, 0);
}
// ================= 绘制:HUD =================
function drawHUD() {
if (state.mode === 'menu') return;
const total = Math.floor(state.meters) + state.score;
const d = difficulty();
ctx.save();
ctx.textAlign = 'left';
ctx.fillStyle = '#e8f6ff';
ctx.font = 'bold 30px system-ui, sans-serif';
ctx.fillText(Math.floor(state.meters) + ' m', 28, 52);
ctx.font = 'bold 17px system-ui, sans-serif';
ctx.fillStyle = '#6ef0ff';
ctx.fillText('得分 ' + total, 28, 80);
ctx.font = '13px system-ui, sans-serif';
ctx.fillStyle = 'rgba(255, 170, 210, .75)';
ctx.fillText('击落 ' + state.kills, 28, 104);
ctx.textAlign = 'right';
ctx.font = '13px system-ui, sans-serif';
ctx.fillStyle = 'rgba(170, 215, 255, .62)';
ctx.fillText('最高 ' + state.best, W - 28, 30);
const bw = 150, bh = 8;
const bx = W - bw - 28, by = 44;
ctx.fillStyle = 'rgba(255, 255, 255, .12)';
roundRect(ctx, bx, by, bw, bh, 4);
ctx.fill();
if (d > 0.01) {
const g = ctx.createLinearGradient(bx, 0, bx + bw, 0);
g.addColorStop(0, '#4de2ff');
g.addColorStop(0.55, '#b06bff');
g.addColorStop(1, '#ff4d6d');
ctx.fillStyle = g;
roundRect(ctx, bx, by, bw * d, bh, 4);
ctx.fill();
}
ctx.textAlign = 'right';
ctx.font = '12px system-ui, sans-serif';
ctx.fillStyle = 'rgba(170, 215, 255, .55)';
ctx.fillText('难度', bx + bw, by - 8);
ctx.restore();
}
// ================= 绘制:覆盖层 =================
function drawOverlay() {
if (state.mode === 'playing') return;
ctx.fillStyle = 'rgba(5, 7, 20, .76)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
if (state.mode === 'menu') {
ctx.save();
ctx.shadowBlur = 32;
ctx.shadowColor = '#4de2ff';
ctx.fillStyle = '#6ef0ff';
ctx.font = 'bold 54px system-ui, sans-serif';
ctx.fillText('疾风跑酷', W / 2, 168);
ctx.restore();
ctx.fillStyle = 'rgba(220, 240, 255, .86)';
ctx.font = '17px system-ui, sans-serif';
ctx.fillText('左手释放能量弹,拦截飞来的敌方弹幕', W / 2, 228);
ctx.fillText('右手跳跃躲地面障碍,按住滑行穿过悬挂障碍', W / 2, 260);
ctx.fillStyle = 'rgba(150, 220, 255, .7)';
ctx.font = '14px system-ui, sans-serif';
ctx.fillText('竖屏拿手机时画面会自动旋转为横屏', W / 2, 296);
ctx.fillStyle = 'rgba(255, 160, 200, .82)';
ctx.font = '15px system-ui, sans-serif';
ctx.fillText('速度越来越快,弹幕越来越密 —— 你能跑多远?', W / 2, 332);
ctx.fillStyle = '#ff5ea8';
ctx.font = 'bold 22px system-ui, sans-serif';
ctx.fillText('点击屏幕开始', W / 2, 406);
} else {
ctx.save();
ctx.shadowBlur = 28;
ctx.shadowColor = '#ff2d55';
ctx.fillStyle = '#ff5ea8';
ctx.font = 'bold 48px system-ui, sans-serif';
ctx.fillText('撞车了!', W / 2, 150);
ctx.restore();
const total = Math.floor(state.meters) + state.score;
ctx.fillStyle = '#e8f6ff';
ctx.font = 'bold 46px system-ui, sans-serif';
ctx.fillText(String(total), W / 2, 220);
ctx.fillStyle = 'rgba(170, 215, 255, .7)';
ctx.font = '16px system-ui, sans-serif';
ctx.fillText('总分(距离 ' + Math.floor(state.meters) + ' m + 击落 ' + state.kills + ' × 50)', W / 2, 254);
const isNew = total >= state.best && total > 0;
ctx.fillStyle = isNew ? '#6ef0ff' : 'rgba(170, 215, 255, .7)';
ctx.font = 'bold 17px system-ui, sans-serif';
ctx.fillText(isNew ? '新纪录!' : '最高纪录 ' + state.best, W / 2, 292);
if (state.overTimer > 0.6) {
ctx.fillStyle = '#ff5ea8';
ctx.font = 'bold 19px system-ui, sans-serif';
ctx.fillText('点击屏幕重新开始', W / 2, 370);
}
}
}
// ================= 渲染 =================
function render() {
ctx.save();
if (state.shake > 0) {
ctx.translate(
(Math.random() - 0.5) * state.shake,
(Math.random() - 0.5) * state.shake
);
}
drawSky();
drawStars();
drawClouds();
drawSkyline(0.10, 'rgba(18, 22, 52, .95)', GROUND_Y, 130, 70);
drawSkyline(0.28, 'rgba(10, 13, 34, .98)', GROUND_Y, 88, 54);
drawGround();
drawObstacles();
drawEnemyBullets();
drawPlayerBullets();
drawParticles();
if (state.mode !== 'over') drawPlayer();
drawHUD();
drawButtons();
drawOverlay();
ctx.restore();
}
// ================= 输入处理 =================
function handleTap(p, pointerId) {
if (state.mode === 'menu') {
restart();
return;
}
if (state.mode === 'over') {
if (state.overTimer > 0.6) restart();
return;
}
if (inCircle(p, BTN.skill)) {
pressed.skill = true;
fireSkill();
return;
}
if (inCircle(p, BTN.jump)) {
pressed.jump = true;
doJump();
return;
}
if (inCircle(p, BTN.slide)) {
pressed.slide = true;
slidePointers.add(pointerId);
player.sliding = true;
return;
}
// 区域兜底:左半屏技能,右半屏跳跃
if (p.x < W / 2) {
pressed.skill = true;
fireSkill();
} else {
pressed.jump = true;
doJump();
}
}
document.addEventListener('pointerdown', function (e) {
e.preventDefault();
const p = getPos(e);
const inside = p.x >= 0 && p.x <= W && p.y >= 0 && p.y <= H;
// 游戏中只响应画面内的点击;菜单/结算允许点黑边
if (state.mode === 'playing' && !inside) return;
handleTap(p, e.pointerId);
});
function releasePointer(e) {
pressed.skill = false;
pressed.jump = false;
if (slidePointers.has(e.pointerId)) {
slidePointers.delete(e.pointerId);
if (slidePointers.size === 0) {
player.sliding = false;
pressed.slide = false;
}
}
}
window.addEventListener('pointerup', releasePointer);
window.addEventListener('pointercancel', releasePointer);
// 键盘(电脑调试用)
window.addEventListener('keydown', function (e) {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
e.preventDefault();
if (!e.repeat) doJump();
pressed.jump = true;
}
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
e.preventDefault();
if (state.mode === 'playing') player.sliding = true;
pressed.slide = true;
}
if (e.code === 'KeyJ' || e.code === 'KeyK' || e.code === 'KeyF' || e.code === 'ShiftLeft') {
e.preventDefault();
if (!e.repeat) fireSkill();
pressed.skill = true;
}
if (e.code === 'KeyR' && state.mode !== 'menu') {
restart();
}
});
window.addEventListener('keyup', function (e) {
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
player.sliding = false;
pressed.slide = false;
}
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') {
pressed.jump = false;
}
if (e.code === 'KeyJ' || e.code === 'KeyK' || e.code === 'KeyF' || e.code === 'ShiftLeft') {
pressed.skill = false;
}
});
window.addEventListener('blur', function () {
player.sliding = false;
slidePointers.clear();
pressed.skill = pressed.jump = pressed.slide = false;
});
// ================= 主循环 =================
let last = performance.now();
function loop(now) {
let dt = (now - last) / 1000;
last = now;
if (dt > 0.05) dt = 0.05;
update(dt);
render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>Game Source: 疾风跑酷 · 自动横屏
Creator: LaserPirate33
Libraries: none
Complexity: complex (1213 lines, 34.2 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-laserpirate33" to link back to the original. Then publish at arcadelab.ai/publish.