昨天、今天和明天
by LuckyKoala29817 lines30.5 KB
<!DOCTYPE html>
<html lang="zh">
<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>
* { margin: 0; padding: 0; }
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
touch-action: none; /* 禁止触摸滚动 */
}
canvas {
display: block;
background: #0a0a0a;
cursor: default;
border: 1px solid #222;
image-rendering: pixelated;
touch-action: none;
}
/* 移动端控制按钮 */
.control-btn {
position: fixed;
bottom: 40px;
width: 70px;
height: 70px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
border: 2px solid rgba(255, 255, 255, 0.25);
backdrop-filter: blur(4px);
display: none; /* 默认隐藏,游戏开始时显示 */
align-items: center;
justify-content: center;
font-size: 36px;
color: rgba(255, 255, 255, 0.6);
user-select: none;
touch-action: none;
z-index: 20;
transition: background 0.1s;
}
.control-btn:active {
background: rgba(255, 255, 255, 0.2);
}
#btn-left {
left: 30px;
}
#btn-right {
right: 30px;
}
/* 重来按钮(与画布同层) */
.restart-btn {
position: fixed; /* 改为fixed,覆盖全屏 */
bottom: 120px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.08);
border: 1px solid #666;
color: #ccc;
padding: 12px 36px;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
font-size: 18px;
cursor: pointer;
border-radius: 30px;
transition: 0.3s;
backdrop-filter: blur(4px);
display: none;
letter-spacing: 2px;
z-index: 30;
}
.restart-btn:hover { background: rgba(255, 255, 255, 0.2); color: #fff; border-color: #aaa; }
/* 针对移动端调整按钮大小 */
@media (max-width: 600px) {
.control-btn {
width: 60px;
height: 60px;
font-size: 30px;
bottom: 30px;
}
#btn-left { left: 20px; }
#btn-right { right: 20px; }
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<!-- 触屏控制按钮 -->
<div class="control-btn" id="btn-left">◀</div>
<div class="control-btn" id="btn-right">▶</div>
<button class="restart-btn" id="restartBtn">↻ 重来</button>
<script>
// ---------- 固定逻辑尺寸 ----------
const DESIGN_W = 900;
const DESIGN_H = 600;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = DESIGN_W;
canvas.height = DESIGN_H;
// ---------- 离屏画布 ----------
const offscreen = document.createElement('canvas');
offscreen.width = DESIGN_W;
offscreen.height = DESIGN_H;
const offCtx = offscreen.getContext('2d');
// ---------- 自适应尺寸(保持 3:2) ----------
function resizeCanvas() {
const winW = window.innerWidth;
const winH = window.innerHeight;
const scaleX = winW / DESIGN_W;
const scaleY = winH / DESIGN_H;
let scale = Math.min(scaleX, scaleY) * 0.96;
canvas.style.width = (DESIGN_W * scale) + 'px';
canvas.style.height = (DESIGN_H * scale) + 'px';
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ---------- 鼠标/触摸坐标映射 ----------
let mouseX = 0, mouseY = 0;
let mouseOnMenu = false;
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
mouseX = (e.clientX - rect.left) * scaleX;
mouseY = (e.clientY - rect.top) * scaleY;
if (gameState === STATE.MENU) {
const text = '昨天、今天和明天';
ctx.font = '40px "Microsoft YaHei", "PingFang SC", sans-serif';
const metrics = ctx.measureText(text);
const tw = metrics.width;
const th = 50;
const cx = canvas.width/2;
const cy = canvas.height/2;
mouseOnMenu = (mouseX >= cx - tw/2 && mouseX <= cx + tw/2 &&
mouseY >= cy - th/2 && mouseY <= cy + th/2);
canvas.style.cursor = mouseOnMenu ? 'pointer' : 'default';
}
});
canvas.addEventListener('click', (e) => {
if (gameState === STATE.MENU && mouseOnMenu) {
startGame();
}
});
// ---------- 游戏状态 ----------
const STATE = { MENU: 0, PLAYING: 1, GAMEOVER: 2 };
let gameState = STATE.MENU;
let deathCause = 'monster';
const game = {
player: { x: 0, y: 0, radius: 18 },
gate: { x: 350, y: 0 },
currentLightRadius: 220,
speed: 3.5,
isMoving: false,
monsters: [],
monsterSpawnTimer: 0,
spawnInterval: 45,
minX: -800,
maxX: 800,
minY: -150,
maxY: 150,
isGameOver: false,
gameOverAlpha: 0,
frame: 0,
groundOffset: 0,
leftHoldTime: 0,
psycheStage: 0,
prevPsycheStage: -1,
psycheAlpha: 0,
psycheDisplayTimer: 0,
floatPhase: 0,
arrowFlowOffset: 0,
};
// ---------- 键盘 ----------
const keys = { left: false, right: false, up: false, down: false };
document.addEventListener('keydown', (e) => {
if (gameState !== STATE.PLAYING) return;
if (e.key === 'ArrowLeft') { keys.left = true; e.preventDefault(); }
if (e.key === 'ArrowRight') { keys.right = true; e.preventDefault(); }
if (e.key === 'ArrowUp') { keys.up = true; e.preventDefault(); }
if (e.key === 'ArrowDown') { keys.down = true; e.preventDefault(); }
if (e.key === 'w' || e.key === 'W') { keys.up = true; e.preventDefault(); }
if (e.key === 's' || e.key === 'S') { keys.down = true; e.preventDefault(); }
if (e.key === 'r' || e.key === 'R') { restartGame(); }
});
document.addEventListener('keyup', (e) => {
if (gameState !== STATE.PLAYING) return;
if (e.key === 'ArrowLeft') { keys.left = false; e.preventDefault(); }
if (e.key === 'ArrowRight') { keys.right = false; e.preventDefault(); }
if (e.key === 'ArrowUp') { keys.up = false; e.preventDefault(); }
if (e.key === 'ArrowDown') { keys.down = false; e.preventDefault(); }
if (e.key === 'w' || e.key === 'W') { keys.up = false; e.preventDefault(); }
if (e.key === 's' || e.key === 'S') { keys.down = false; e.preventDefault(); }
});
// ---------- 触摸按钮控制 ----------
const btnLeft = document.getElementById('btn-left');
const btnRight = document.getElementById('btn-right');
function setupTouchButton(element, key) {
// 触摸事件
element.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameState === STATE.PLAYING) {
keys[key] = true;
}
}, { passive: false });
element.addEventListener('touchend', (e) => {
e.preventDefault();
keys[key] = false;
}, { passive: false });
element.addEventListener('touchcancel', (e) => {
keys[key] = false;
});
// 鼠标支持(便于调试)
element.addEventListener('mousedown', (e) => {
e.preventDefault();
if (gameState === STATE.PLAYING) {
keys[key] = true;
}
});
element.addEventListener('mouseup', (e) => {
e.preventDefault();
keys[key] = false;
});
element.addEventListener('mouseleave', () => {
keys[key] = false;
});
}
setupTouchButton(btnLeft, 'left');
setupTouchButton(btnRight, 'right');
// 防止触摸时页面滚动
document.addEventListener('touchmove', (e) => {
if (e.target.closest('canvas') || e.target.closest('.control-btn')) {
e.preventDefault();
}
}, { passive: false });
// ---------- 辅助 ----------
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
// 文字怪物池
const monsterTexts = [
'我好累', '好想休息', '好困', '想睡觉', '作业还没做',
'作业什么时候要交', '实习怎么办', '科二科三考了吗?',
'大学了也得认真学习', '在干什么?', '吃什么?',
'怎么这么久不找爸爸妈妈聊天?',
'周末有没有出去玩?', '这么晚才起床?', '昨天几点睡的?',
'什么时候放假?', '车票订好了吗?', '为什么又不回我消息?',
'别天天待宿舍躺着'
];
function spawnMonster() {
const angle = Math.random() * Math.PI * 2;
const spawnDist = 500 + Math.random() * 200;
const x = game.player.x + Math.cos(angle) * spawnDist;
const y = game.player.y + Math.sin(angle) * spawnDist * 0.6;
const text = monsterTexts[Math.floor(Math.random() * monsterTexts.length)];
return {
x, y,
radius: 12 + Math.random() * 14,
speed: 0.25,
wobble: Math.random() * 100,
text: text,
color: `hsl(${Math.random() * 60 + 300}, 80%, ${60 + Math.random() * 30}%)`,
alpha: 0,
seed: Math.random() * 1000,
};
}
function startGame() {
gameState = STATE.PLAYING;
document.getElementById('restartBtn').style.display = 'none';
// 显示触摸按钮
btnLeft.style.display = 'flex';
btnRight.style.display = 'flex';
game.player.x = 0;
game.player.y = 0;
game.gate.x = 350;
game.gate.y = 0;
game.monsters = [];
game.isGameOver = false;
game.gameOverAlpha = 0;
game.currentLightRadius = 220;
game.monsterSpawnTimer = 10;
game.groundOffset = 0;
game.leftHoldTime = 0;
game.psycheStage = 0;
game.prevPsycheStage = -1;
game.psycheAlpha = 0;
game.psycheDisplayTimer = 0;
game.floatPhase = 0;
game.arrowFlowOffset = 0;
keys.left = false;
keys.right = false;
keys.up = false;
keys.down = false;
deathCause = 'monster';
canvas.style.cursor = 'none';
}
// ---------- 更新 ----------
function update() {
if (gameState === STATE.MENU) return;
if (gameState === STATE.GAMEOVER) {
game.gameOverAlpha = Math.min(1, game.gameOverAlpha + 0.02);
return;
}
const player = game.player;
const gate = game.gate;
game.frame++;
const deltaTime = 1 / 60;
let moveX = 0, moveY = 0;
if (keys.left) moveX = -game.speed;
if (keys.right) moveX = game.speed;
if (keys.up) moveY = -game.speed * 0.8;
if (keys.down) moveY = game.speed * 0.8;
game.isMoving = (moveX !== 0 || moveY !== 0);
if (moveX > 0) {
player.x += moveX;
gate.x += moveX;
} else if (moveX < 0) {
player.x += moveX;
}
if (moveY !== 0) {
player.y += moveY;
player.y = Math.min(game.maxY, Math.max(game.minY, player.y));
}
player.x = Math.min(game.maxX, Math.max(game.minX, player.x));
gate.x = Math.min(1000, Math.max(-200, gate.x));
if (keys.left || keys.right) {
const dir = keys.left ? -1 : 1;
game.groundOffset += dir * game.speed * 0.8;
}
const leftBound = game.minX;
const rightBound = gate.x;
let t = (player.x - leftBound) / (rightBound - leftBound);
t = Math.max(0, Math.min(1, t));
game.currentLightRadius = t * 220;
// ----- 心理攻击时间(30s, 60s, 90s,每段显示5秒后消失) -----
const isLeft = keys.left;
if (isLeft) {
game.leftHoldTime += deltaTime;
} else {
game.leftHoldTime = 0;
game.psycheStage = 0;
game.psycheDisplayTimer = 0;
}
if (game.psycheStage === 0 && game.leftHoldTime >= 30) {
game.psycheStage = 1;
game.psycheDisplayTimer = 0;
} else if (game.psycheStage === 0 && game.leftHoldTime >= 60) {
game.psycheStage = 2;
game.psycheDisplayTimer = 0;
} else if (game.psycheStage === 0 && game.leftHoldTime >= 90) {
game.psycheStage = 3;
game.psycheDisplayTimer = 0;
}
if (game.psycheStage > 0 && keys.left) {
game.psycheDisplayTimer += deltaTime;
if (game.psycheDisplayTimer >= 5) {
game.psycheStage = 0;
game.psycheDisplayTimer = 0;
}
}
if (game.psycheStage > 0 && keys.left) {
game.psycheAlpha = Math.min(0.45, game.psycheAlpha + deltaTime * 0.6);
} else {
game.psycheAlpha = Math.max(0, game.psycheAlpha - deltaTime * 0.8);
}
if (game.leftHoldTime >= 100 && game.psycheStage === 0) {
deathCause = 'left';
game.isGameOver = true;
gameState = STATE.GAMEOVER;
document.getElementById('restartBtn').style.display = 'block';
btnLeft.style.display = 'none';
btnRight.style.display = 'none';
return;
}
if (game.isMoving) {
const flowSpeed = Math.abs(moveX) * 0.005;
game.arrowFlowOffset += flowSpeed * deltaTime * 60;
}
if (!game.isMoving) {
game.monsterSpawnTimer--;
if (game.monsterSpawnTimer <= 0) {
if (game.monsters.length < 30) {
game.monsters.push(spawnMonster());
}
game.monsterSpawnTimer = game.spawnInterval;
}
} else {
game.monsters = [];
game.monsterSpawnTimer = 15;
}
for (let i = game.monsters.length - 1; i >= 0; i--) {
const mon = game.monsters[i];
if (mon.alpha < 1) {
mon.alpha = Math.min(1, mon.alpha + deltaTime * 1.5);
}
if (!game.isMoving) {
const dx = player.x - mon.x;
const dy = player.y - mon.y;
const d = Math.hypot(dx, dy);
if (d > 0.5) {
mon.x += (dx / d) * mon.speed;
mon.y += (dy / d) * mon.speed * 0.8;
}
mon.wobble += 0.08;
mon.x += Math.sin(mon.wobble) * 0.15;
}
if (dist(mon, player) < player.radius + mon.radius) {
deathCause = 'monster';
game.isGameOver = true;
gameState = STATE.GAMEOVER;
document.getElementById('restartBtn').style.display = 'block';
btnLeft.style.display = 'none';
btnRight.style.display = 'none';
return;
}
if (Math.abs(mon.x - player.x) > 800 || Math.abs(mon.y - player.y) > 500) {
game.monsters.splice(i, 1);
}
}
if (game.isMoving) {
game.floatPhase += deltaTime * 1.5;
}
}
// ---------- 渲染 ----------
function draw() {
offCtx.clearRect(0, 0, DESIGN_W, DESIGN_H);
const drawCtx = offCtx;
const cx = DESIGN_W / 2;
const cy = DESIGN_H / 2;
const player = game.player;
const gate = game.gate;
const radius = game.currentLightRadius;
// 背景
const bgGrad = drawCtx.createRadialGradient(cx, cy, 10, cx, cy, 500);
bgGrad.addColorStop(0, '#111');
bgGrad.addColorStop(0.6, '#050505');
bgGrad.addColorStop(1, '#000000');
drawCtx.fillStyle = bgGrad;
drawCtx.fillRect(0, 0, DESIGN_W, DESIGN_H);
if (gameState === STATE.MENU) {
const text = '昨天、今天和明天';
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
drawCtx.font = '40px "Microsoft YaHei", "PingFang SC", sans-serif';
const color = mouseOnMenu ? '#888' : '#eee';
drawCtx.shadowBlur = 40;
drawCtx.shadowColor = '#aaa';
drawCtx.fillStyle = color;
drawCtx.fillText(text, cx, cy);
drawCtx.shadowBlur = 0;
if (!mouseOnMenu) {
drawCtx.fillStyle = '#555';
drawCtx.font = '18px sans-serif';
drawCtx.fillText('点击开始', cx, cy + 60);
}
// 菜单不抖动,直接绘制
ctx.clearRect(0, 0, DESIGN_W, DESIGN_H);
ctx.drawImage(offscreen, 0, 0);
return;
}
// 地面
const offset = game.groundOffset % 60;
drawCtx.strokeStyle = 'rgba(80, 100, 120, 0.15)';
drawCtx.lineWidth = 2;
drawCtx.setLineDash([8, 16]);
drawCtx.lineDashOffset = -offset;
for (let y = 0; y < DESIGN_H; y += 40) {
drawCtx.beginPath();
drawCtx.moveTo(0, y);
drawCtx.lineTo(DESIGN_W, y);
drawCtx.stroke();
}
drawCtx.strokeStyle = 'rgba(60, 80, 100, 0.08)';
drawCtx.lineWidth = 1;
drawCtx.setLineDash([4, 20]);
drawCtx.lineDashOffset = -offset * 0.3;
for (let x = 0; x < DESIGN_W; x += 50) {
drawCtx.beginPath();
drawCtx.moveTo(x, 0);
drawCtx.lineTo(x, DESIGN_H);
drawCtx.stroke();
}
drawCtx.setLineDash([]);
// 光圈
if (radius > 0) {
const time = Date.now() / 1000;
const flicker = 0.8 + 0.2 * Math.sin(time * 1.5 + 0.5);
const alphaMul = 0.7 + 0.3 * flicker;
const fogGrad = drawCtx.createRadialGradient(cx, cy, 5, cx, cy, Math.max(radius * 1.3, 20));
fogGrad.addColorStop(0, `rgba(200, 220, 255, ${(0.15 + radius/500) * alphaMul})`);
fogGrad.addColorStop(0.4, `rgba(120, 150, 200, ${(0.08 + radius/600) * alphaMul})`);
fogGrad.addColorStop(0.8, 'rgba(20, 30, 50, 0.0)');
fogGrad.addColorStop(1, 'rgba(0,0,0,0)');
drawCtx.fillStyle = fogGrad;
drawCtx.beginPath();
drawCtx.arc(cx, cy, Math.max(radius * 1.3, 20), 0, Math.PI * 2);
drawCtx.fill();
}
// 光门(“美好的明天”半透明忽明忽暗)
if (radius > 0) {
const gx = cx + (gate.x - player.x);
const gy = cy + (gate.y - player.y);
const gateGlow = drawCtx.createRadialGradient(gx, gy, 5, gx, gy, 120);
gateGlow.addColorStop(0, 'rgba(255, 255, 255, 0.4)');
gateGlow.addColorStop(0.3, 'rgba(200, 230, 255, 0.15)');
gateGlow.addColorStop(1, 'rgba(255, 255, 255, 0)');
drawCtx.fillStyle = gateGlow;
drawCtx.beginPath();
drawCtx.arc(gx, gy, 120, 0, Math.PI * 2);
drawCtx.fill();
const chars = ['美', '好', '的', '明', '天'];
const fontSize = 30;
const charSpacing = fontSize * 0.9;
const totalHeight = chars.length * charSpacing;
const startY = gy - totalHeight / 2;
drawCtx.font = `bold ${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`;
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
const time = Date.now() / 1000;
const alphaBase = 0.55 + 0.3 * Math.sin(time * 0.8 + 0.2);
drawCtx.shadowColor = '#fff';
drawCtx.shadowBlur = 40;
chars.forEach((char, index) => {
const yPos = startY + index * charSpacing;
const waveX = Math.sin(time * 1.5 + index * 1.2) * 8;
const waveY = Math.cos(time * 1.1 + index * 1.8) * 5;
drawCtx.save();
drawCtx.globalAlpha = alphaBase;
drawCtx.translate(gx + waveX, yPos + waveY);
drawCtx.rotate(Math.sin(time * 0.6 + index * 0.5) * 0.08);
drawCtx.shadowBlur = 50 + Math.sin(time + index) * 10;
drawCtx.fillStyle = '#ffffff';
drawCtx.fillText(char, 0, 0);
drawCtx.restore();
});
}
// 光球
let px = cx, py = cy;
if (radius > 0) {
const floatAmplitude = game.isMoving ? 4 : 0;
const floatOffset = Math.sin(game.floatPhase) * floatAmplitude;
px = cx;
py = cy + floatOffset;
const r = game.player.radius;
const leftBound = game.minX;
const rightBound = game.gate.x;
let t = (player.x - leftBound) / (rightBound - leftBound);
t = Math.max(0, Math.min(1, t));
const baseBright = 0.5 + t * 0.5;
const time = Date.now() / 1000;
const flicker = 0.85 + 0.15 * Math.sin(time * 2.0 + 0.3);
const bright = baseBright * flicker;
drawCtx.shadowBlur = 0;
drawCtx.fillStyle = `rgba(220, 235, 255, ${bright})`;
drawCtx.beginPath();
drawCtx.arc(px, py, r, 0, Math.PI * 2);
drawCtx.fill();
}
// 整体变暗遮罩
const leftBound = game.minX;
const rightBound = game.gate.x;
let t2 = (player.x - leftBound) / (rightBound - leftBound);
t2 = Math.max(0, Math.min(1, t2));
const darkAlpha = (1 - t2) * 0.92;
if (darkAlpha > 0.01) {
drawCtx.fillStyle = `rgba(0, 0, 0, ${darkAlpha})`;
drawCtx.fillRect(0, 0, DESIGN_W, DESIGN_H);
}
// 怪物
const timeNow = Date.now() / 1000;
for (const mon of game.monsters) {
const seed = mon.seed || 0;
const nx = Math.sin(timeNow * 0.5 + seed * 2.0) * 6 + Math.sin(timeNow * 0.8 + seed * 3.1) * 4;
const ny = Math.cos(timeNow * 0.6 + seed * 1.7) * 6 + Math.cos(timeNow * 0.9 + seed * 2.8) * 4;
const wobX = Math.sin(mon.wobble) * 2;
const wobY = Math.sin(mon.wobble * 1.3) * 3;
const mx = cx + (mon.x - player.x) + nx + wobX;
const my = cy + (mon.y - player.y) + ny + wobY;
const distToPlayer = dist(mon, player);
let alphaFactor = 1;
if (distToPlayer < radius * 0.6) alphaFactor = 0.25;
else if (distToPlayer < radius) alphaFactor = 0.65;
const finalAlpha = mon.alpha * alphaFactor;
drawCtx.save();
drawCtx.globalAlpha = finalAlpha;
drawCtx.font = '18px "Microsoft YaHei", "PingFang SC", sans-serif';
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
drawCtx.shadowColor = mon.color;
drawCtx.shadowBlur = 15;
drawCtx.fillStyle = mon.color;
const rot = Math.sin(timeNow * 0.3 + seed * 0.5) * 0.06;
drawCtx.translate(mx, my);
drawCtx.rotate(rot);
drawCtx.fillText(mon.text, 0, 0);
drawCtx.restore();
}
// 箭头流
if (radius > 0) {
const gx = cx + (gate.x - player.x);
const gy = cy + (gate.y - player.y);
const dx = gx - px;
const dy = gy - py;
const dist = Math.hypot(dx, dy);
if (dist > 30) {
const numArrows = 8;
const flowOffset = game.arrowFlowOffset % 1;
const time = Date.now() / 1000;
for (let i = 0; i < numArrows; i++) {
let progress = (i / numArrows + flowOffset) % 1;
const ax = px + dx * progress;
const ay = py + dy * progress;
const angle = Math.atan2(dy, dx);
let alphaBase = 0.3;
if (game.isMoving) alphaBase = 0.6;
const flicker = 0.6 + 0.4 * Math.sin(time * 2.5 + i * 0.8);
const alpha = alphaBase * flicker;
const arrowSize = 12;
drawCtx.save();
drawCtx.translate(ax, ay);
drawCtx.rotate(angle);
drawCtx.beginPath();
drawCtx.moveTo(arrowSize, 0);
drawCtx.lineTo(-arrowSize * 0.5, -arrowSize * 0.4);
drawCtx.lineTo(-arrowSize * 0.5, arrowSize * 0.4);
drawCtx.closePath();
drawCtx.strokeStyle = `rgba(200, 220, 255, ${alpha})`;
drawCtx.lineWidth = 2;
drawCtx.stroke();
drawCtx.restore();
}
}
}
// 文字
if (game.psycheStage >= 1 && !game.isGameOver && game.psycheAlpha > 0.01) {
let message = '';
if (game.psycheStage === 1) message = '又在逃避了吗?';
else if (game.psycheStage === 2) message = '......';
else if (game.psycheStage === 3) message = '';
drawCtx.save();
drawCtx.globalAlpha = game.psycheAlpha;
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
drawCtx.shadowBlur = 0;
drawCtx.fillStyle = 'rgba(255, 255, 255, 1)';
drawCtx.font = '28px "Microsoft YaHei", "PingFang SC", sans-serif';
drawCtx.fillText(message, DESIGN_W/2, DESIGN_H/2 - 60);
drawCtx.restore();
}
// 底部提示(仅在游戏进行时)
if (gameState === STATE.PLAYING && !game.isGameOver) {
const bottomY = DESIGN_H - 40;
drawCtx.save();
drawCtx.globalAlpha = 0.4;
drawCtx.font = '20px "Microsoft YaHei", "PingFang SC", sans-serif';
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
drawCtx.fillStyle = '#aaa';
drawCtx.fillText('往前', DESIGN_W/2 - 20, bottomY);
const arrowX = DESIGN_W/2 + 25;
const arrowSize = 16;
drawCtx.beginPath();
drawCtx.moveTo(arrowX + arrowSize, bottomY);
drawCtx.lineTo(arrowX - arrowSize*0.4, bottomY - arrowSize*0.6);
drawCtx.lineTo(arrowX - arrowSize*0.4, bottomY + arrowSize*0.6);
drawCtx.closePath();
drawCtx.fillStyle = '#aaa';
drawCtx.fill();
drawCtx.restore();
}
// 游戏结束
if (gameState === STATE.GAMEOVER) {
drawCtx.fillStyle = 'rgba(0, 0, 0, 0.85)';
drawCtx.fillRect(0, 0, DESIGN_W, DESIGN_H);
const alpha = game.gameOverAlpha;
if (alpha > 0.01) {
drawCtx.save();
drawCtx.globalAlpha = alpha;
drawCtx.textAlign = 'center';
drawCtx.textBaseline = 'middle';
drawCtx.shadowBlur = 0;
drawCtx.fillStyle = '#eee';
drawCtx.font = '28px "Microsoft YaHei", "PingFang SC", sans-serif';
let msg = '';
if (deathCause === 'monster') {
msg = '你为什么就不能再努力一点?';
} else if (deathCause === 'left') {
msg = '好吧,这也是一种选择。';
}
drawCtx.fillText(msg, DESIGN_W/2, DESIGN_H/2);
drawCtx.restore();
}
}
// ---------- 像素抖动 ----------
const scale = 0.6;
const sw = Math.floor(DESIGN_W * scale);
const sh = Math.floor(DESIGN_H * scale);
const tempCanvas = document.createElement('canvas');
tempCanvas.width = sw;
tempCanvas.height = sh;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(offscreen, 0, 0, sw, sh);
ctx.clearRect(0, 0, DESIGN_W, DESIGN_H);
ctx.imageSmoothingEnabled = false;
const jitterX = (Math.random() - 0.5) * 4;
const jitterY = (Math.random() - 0.5) * 4;
ctx.drawImage(tempCanvas, jitterX, jitterY, DESIGN_W, DESIGN_H);
}
// ---------- 循环 ----------
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// ---------- 重启(回到菜单) ----------
function restartGame() {
gameState = STATE.MENU;
document.getElementById('restartBtn').style.display = 'none';
btnLeft.style.display = 'none';
btnRight.style.display = 'none';
canvas.style.cursor = 'default';
game.player.x = 0;
game.player.y = 0;
game.gate.x = 350;
game.gate.y = 0;
game.monsters = [];
game.isGameOver = false;
game.gameOverAlpha = 0;
game.currentLightRadius = 220;
game.monsterSpawnTimer = 10;
game.groundOffset = 0;
game.leftHoldTime = 0;
game.psycheStage = 0;
game.prevPsycheStage = -1;
game.psycheAlpha = 0;
game.psycheDisplayTimer = 0;
game.floatPhase = 0;
game.arrowFlowOffset = 0;
keys.left = false;
keys.right = false;
keys.up = false;
keys.down = false;
deathCause = 'monster';
}
document.getElementById('restartBtn').addEventListener('click', restartGame);
// ---------- 启动 ----------
gameLoop();
</script>
</body>
</html>Game Source: 昨天、今天和明天
Creator: LuckyKoala29
Libraries: none
Complexity: complex (817 lines, 30.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-luckykoala29" to link back to the original. Then publish at arcadelab.ai/publish.