不知名提交

This commit is contained in:
2025-12-13 20:53:50 +08:00
parent c147502b4d
commit 1221d6faf1
120 changed files with 11005 additions and 1092 deletions

View File

@@ -0,0 +1,314 @@
// 清新跑酷 - Endless Runner (Mobile Portrait, Touch-friendly)
(() => {
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const restartBtn = document.getElementById('restartBtn');
const overlay = document.getElementById('overlay');
const overlayRestart = document.getElementById('overlayRestart');
const finalScoreEl = document.getElementById('finalScore');
let dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
let running = true;
let gameOver = false;
let lastTime = performance.now();
let elapsed = 0; // seconds
let score = 0;
const world = {
width: 360,
height: 640,
groundH: 90, // 地面高度CSS像素
baseSpeed: 240, // 初始速度px/s
speed: 240, // 当前速度(随难度提升)
gravity: 1800, // 重力px/s^2
jumpV: -864, // 跳跃初速度px/s
};
const player = {
x: 72,
y: 0, // 通过 resetPlayer 设置
w: 44,
h: 54,
vy: 0,
grounded: false,
color: '#2f7d5f'
};
const obstacles = [];
const coins = [];
let obstacleTimer = 0; // ms 到下一个障碍
let coinTimer = 0; // ms 到下一个道具
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(min, max) { return Math.random() * (max - min) + min; }
function resizeCanvas() {
dpr = Math.max(1, Math.floor(window.devicePixelRatio || 1));
const cssWidth = Math.min(480, document.documentElement.clientWidth);
const cssHeight = document.documentElement.clientHeight;
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
canvas.width = Math.floor(cssWidth * dpr);
canvas.height = Math.floor(cssHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // 使用CSS像素绘制
world.width = cssWidth;
world.height = cssHeight;
world.groundH = Math.max(64, Math.floor(world.height * 0.14));
resetPlayer();
}
function resetPlayer() {
player.y = world.height - world.groundH - player.h;
player.vy = 0;
player.grounded = true;
}
function spawnObstacle() {
const w = rand(28, 56);
const h = rand(40, clamp(world.height * 0.28, 80, 140));
const y = world.height - world.groundH - h;
obstacles.push({ x: world.width + w, y, w, h, color: '#3ea573' });
// 以一定概率在障碍上方生成一个金币
if (Math.random() < 0.6) {
const cx = world.width + w + rand(10, 40);
const cy = y - rand(28, 56);
coins.push({ x: cx, y: cy, r: 10, color: '#f6c453' });
}
}
function spawnCoin() {
const r = 10;
const yTop = world.height * 0.35; // 道具浮在中上区域
const y = rand(yTop, world.height - world.groundH - 80);
coins.push({ x: world.width + 60, y, r, color: '#f6c453' });
}
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
function circleRectOverlap(cx, cy, r, rx, ry, rw, rh) {
const closestX = clamp(cx, rx, rx + rw);
const closestY = clamp(cy, ry, ry + rh);
const dx = cx - closestX;
const dy = cy - closestY;
return (dx * dx + dy * dy) <= r * r;
}
function jump() {
if (gameOver) return;
if (player.grounded) {
player.vy = world.jumpV;
player.grounded = false;
}
}
function update(dt) {
// 难度递增:速度随时间上涨,生成间隔缩短
elapsed += dt;
world.speed = world.baseSpeed + elapsed * 22; // 每秒加速
obstacleTimer -= dt * 1000;
coinTimer -= dt * 1000;
const minInterval = clamp(1400 - elapsed * 20, 700, 1600); // 障碍间隔(更远)
const coinInterval = clamp(1200 - elapsed * 25, 500, 1200); // 金币间隔
if (obstacleTimer <= 0) {
spawnObstacle();
obstacleTimer = rand(minInterval, minInterval * 1.35);
}
if (coinTimer <= 0) {
spawnCoin();
coinTimer = rand(coinInterval * 0.6, coinInterval);
}
// 玩家物理
player.vy += world.gravity * dt;
player.y += player.vy * dt;
const groundY = world.height - world.groundH - player.h;
if (player.y >= groundY) {
player.y = groundY;
player.vy = 0;
player.grounded = true;
}
// 移动障碍与金币
const dx = world.speed * dt;
for (let i = obstacles.length - 1; i >= 0; i--) {
const ob = obstacles[i];
ob.x -= dx;
if (ob.x + ob.w < 0) obstacles.splice(i, 1);
}
for (let i = coins.length - 1; i >= 0; i--) {
const c = coins[i];
c.x -= dx;
if (c.x + c.r < 0) coins.splice(i, 1);
}
// 碰撞检测:障碍
for (const ob of obstacles) {
if (rectsOverlap(player.x, player.y, player.w, player.h, ob.x, ob.y, ob.w, ob.h)) {
endGame();
return;
}
}
// 拾取金币
for (let i = coins.length - 1; i >= 0; i--) {
const c = coins[i];
if (circleRectOverlap(c.x, c.y, c.r, player.x, player.y, player.w, player.h)) {
score += 100; // 金币加分
coins.splice(i, 1);
}
}
// 距离积分(随速度)
score += Math.floor(world.speed * dt * 0.2);
scoreEl.textContent = String(score);
}
function drawGround() {
const y = world.height - world.groundH;
// 地面阴影渐变
const grad = ctx.createLinearGradient(0, y, 0, world.height);
grad.addColorStop(0, 'rgba(60, 150, 110, 0.35)');
grad.addColorStop(1, 'rgba(60, 150, 110, 0.05)');
ctx.fillStyle = grad;
ctx.fillRect(0, y, world.width, world.groundH);
// 地面纹理线
ctx.strokeStyle = 'rgba(47, 79, 63, 0.25)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(world.width, y);
ctx.stroke();
}
function drawPlayer() {
ctx.fillStyle = player.color;
const r = 8; // 圆角
const x = player.x, y = player.y, w = player.w, h = player.h;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fill();
// 前进指示条
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(x + 6, y + 10, 6, 12);
ctx.fillRect(x + 18, y + 10, 6, 12);
}
function drawObstacles() {
for (const ob of obstacles) {
// 渐变柱体
const g = ctx.createLinearGradient(ob.x, ob.y, ob.x, ob.y + ob.h);
g.addColorStop(0, '#52b985');
g.addColorStop(1, '#3ea573');
ctx.fillStyle = g;
ctx.fillRect(ob.x, ob.y, ob.w, ob.h);
// 顶部高亮
ctx.fillStyle = 'rgba(255,255,255,0.35)';
ctx.fillRect(ob.x, ob.y, ob.w, 4);
}
}
function drawCoins() {
for (const c of coins) {
ctx.beginPath();
ctx.arc(c.x, c.y, c.r, 0, Math.PI * 2);
ctx.fillStyle = c.color;
ctx.fill();
// 外圈高光
ctx.strokeStyle = 'rgba(255,255,255,0.6)';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function draw(now) {
// 清屏CSS负责背景渐变这里仅清理
ctx.clearRect(0, 0, world.width, world.height);
drawGround();
drawPlayer();
drawObstacles();
drawCoins();
// 速度指示(右上角小提示)
ctx.fillStyle = 'rgba(47,79,63,0.35)';
ctx.font = '12px system-ui';
ctx.textAlign = 'right';
ctx.fillText(`速度 ${Math.round(world.speed)}px/s`, world.width - 8, 18);
}
function endGame() {
running = false;
gameOver = true;
finalScoreEl.textContent = String(score);
overlay.hidden = false;
}
function resetGame() {
running = true;
gameOver = false;
obstacles.length = 0;
coins.length = 0;
obstacleTimer = 0;
coinTimer = rand(400, 900);
score = 0;
elapsed = 0;
resetPlayer();
overlay.hidden = true;
lastTime = performance.now();
}
function loop(now) {
const dt = Math.min(0.033, (now - lastTime) / 1000); // 限制最大步长
lastTime = now;
if (running) {
update(dt);
draw(now);
}
requestAnimationFrame(loop);
}
// 输入事件
function onKey(e) {
if (e.code === 'Space' || e.code === 'ArrowUp') {
e.preventDefault();
jump();
}
}
function onPointer() { jump(); }
restartBtn.addEventListener('click', () => {
resetGame();
});
overlayRestart.addEventListener('click', () => {
resetGame();
});
window.addEventListener('keydown', onKey, { passive: false });
window.addEventListener('mousedown', onPointer);
window.addEventListener('touchstart', onPointer, { passive: true });
window.addEventListener('resize', () => {
resizeCanvas();
});
// 初始化并启动
resizeCanvas();
requestAnimationFrame(loop);
})();

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover" />
<title>清新跑酷 - InfoGenie</title>
<meta name="theme-color" content="#bde8c7" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="game-shell">
<header class="hud">
<div class="score">分数 <span id="score">0</span></div>
<button id="restartBtn" class="restart" aria-label="重新开始">重新开始</button>
</header>
<main class="stage">
<canvas id="gameCanvas" aria-label="跑酷游戏画布"></canvas>
<div class="hint">轻触屏幕或按空格跳跃</div>
<div id="overlay" class="overlay" hidden>
<div class="overlay-card">
<div class="gameover-title">游戏结束</div>
<div class="summary">分数 <span id="finalScore">0</span></div>
<button class="overlay-restart" id="overlayRestart">重新开始</button>
</div>
</div>
</main>
</div>
<script src="./game.js" defer></script>
</body>
</html>

View File

@@ -0,0 +1,130 @@
/* 清新淡绿色渐变风格与移动端适配 */
:root {
--green-1: #a8e6cf; /* 淡绿色 */
--green-2: #dcedc1; /* 淡黄绿色 */
--accent: #58c48b; /* 按钮主色 */
--accent-dark: #3ca16c;
--text: #2f4f3f; /* 深绿文字 */
--card: #ffffffd9; /* 半透明卡片 */
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
color: var(--text);
background: linear-gradient(180deg, var(--green-1) 0%, var(--green-2) 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.game-shell {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.hud {
position: sticky;
top: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background: color-mix(in oklab, var(--green-2) 65%, white 35%);
backdrop-filter: saturate(1.4) blur(6px);
box-shadow: 0 2px 10px rgb(0 0 0 / 6%);
}
.score {
font-weight: 700;
font-size: 18px;
letter-spacing: 0.5px;
}
.restart {
appearance: none;
border: none;
outline: none;
background: var(--accent);
color: #fff;
font-weight: 600;
font-size: 14px;
padding: 8px 12px;
border-radius: 999px;
box-shadow: 0 4px 12px rgb(88 196 139 / 30%);
transition: transform .05s ease, background .2s ease;
}
.restart:active { transform: scale(0.98); }
.restart:hover { background: var(--accent-dark); }
.stage {
position: relative;
flex: 1;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
.hint {
position: absolute;
bottom: max(10px, env(safe-area-inset-bottom));
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: color-mix(in oklab, var(--text), white 35%);
opacity: 0.85;
pointer-events: none;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgb(0 0 0 / 10%);
}
.overlay[hidden] { display: none; }
.overlay-card {
width: min(88vw, 420px);
background: var(--card);
border-radius: 16px;
box-shadow: 0 10px 30px rgb(0 0 0 / 15%);
padding: 18px 18px 16px;
text-align: center;
}
.gameover-title {
font-size: 20px;
font-weight: 800;
}
.summary {
margin: 12px 0 16px;
font-size: 16px;
}
.overlay-restart {
appearance: none;
border: none;
outline: none;
background: var(--accent);
color: #fff;
font-weight: 700;
font-size: 16px;
padding: 10px 16px;
border-radius: 12px;
box-shadow: 0 6px 16px rgb(88 196 139 / 38%);
}
.overlay-restart:active { transform: scale(0.98); }
.overlay-restart:hover { background: var(--accent-dark); }
@media (prefers-reduced-motion: reduce) {
.restart, .overlay-restart { transition: none; }
}