這樓用來發一些AI寫的小遊戲~
仿懷舊太空射擊遊戲

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dragon Space Shooter</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at top, #1a2342 0%, #090d18 55%, #03050a 100%);
color: #f3f4f6;
font-family: Arial, "Noto Sans TC", sans-serif;
}
.wrap {
text-align: center;
padding: 16px;
}
h1 {
margin: 0 0 10px;
font-size: 28px;
letter-spacing: 1px;
}
.hint {
margin: 0 0 12px;
color: #cbd5e1;
font-size: 14px;
}
canvas {
display: block;
margin: 0 auto;
border: 3px solid #7dd3fc;
border-radius: 12px;
background: linear-gradient(to bottom, #07111f, #08101a 45%, #03060d);
box-shadow: 0 0 24px rgba(125, 211, 252, 0.25);
image-rendering: pixelated;
}
.controls {
margin-top: 14px;
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
button {
border: none;
background: #f97316;
color: white;
padding: 10px 18px;
border-radius: 999px;
font-size: 15px;
cursor: pointer;
font-weight: bold;
box-shadow: 0 6px 14px rgba(249, 115, 22, 0.3);
}
button:hover { filter: brightness(1.08); }
.note {
margin-top: 10px;
font-size: 13px;
color: #94a3b8;
}
</style>
</head>
<body>
<div class="wrap">
<h1>🐉 Dragon Space Shooter</h1>
<p class="hint">操作:← → 移動,空白鍵發射火球</p>
<canvas id="game" width="480" height="640"></canvas>
<div class="controls">
<button id="restartBtn">重新開始</button>
</div>
<div class="note">復古 2D 太空射擊風格,單一 HTML 即可運行</div>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const restartBtn = document.getElementById('restartBtn');
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
const keys = {
ArrowLeft: false,
ArrowRight: false,
Space: false,
};
let stars = [];
let player;
let bullets;
let enemies;
let score;
let gameOver;
let enemySpawnTimer;
let enemySpawnInterval;
let fireCooldown;
let animationId;
function resetGame() {
player = {
x: WIDTH / 2 - 20,
y: HEIGHT - 90,
width: 40,
height: 48,
speed: 5.5,
};
bullets = [];
enemies = [];
score = 0;
gameOver = false;
enemySpawnTimer = 0;
enemySpawnInterval = 42;
fireCooldown = 0;
stars = Array.from({ length: 70 }, () => ({
x: Math.random() * WIDTH,
y: Math.random() * HEIGHT,
size: Math.random() * 2 + 1,
speed: Math.random() * 1.4 + 0.6,
}));
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function rectsOverlap(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
function spawnEnemy() {
const size = 26 + Math.random() * 18;
enemies.push({
x: Math.random() * (WIDTH - size),
y: -size,
width: size,
height: size,
speed: 1.6 + Math.random() * 2.3 + Math.min(score * 0.02, 2.2),
wobble: Math.random() * Math.PI * 2,
wobbleSpeed: 0.03 + Math.random() * 0.04,
});
}
function shoot() {
bullets.push({
x: player.x + player.width / 2 - 4,
y: player.y + 8,
width: 8,
height: 18,
speed: 8.5,
});
}
function update() {
updateStars();
if (gameOver) return;
if (keys.ArrowLeft) player.x -= player.speed;
if (keys.ArrowRight) player.x += player.speed;
player.x = clamp(player.x, 0, WIDTH - player.width);
if (fireCooldown > 0) fireCooldown--;
if (keys.Space && fireCooldown <= 0) {
shoot();
fireCooldown = 10;
}
bullets.forEach((bullet) => {
bullet.y -= bullet.speed;
});
bullets = bullets.filter((bullet) => bullet.y + bullet.height > 0);
enemySpawnTimer++;
if (enemySpawnTimer >= enemySpawnInterval) {
spawnEnemy();
enemySpawnTimer = 0;
enemySpawnInterval = Math.max(18, 42 - Math.floor(score / 5));
}
enemies.forEach((enemy) => {
enemy.y += enemy.speed;
enemy.wobble += enemy.wobbleSpeed;
enemy.x += Math.sin(enemy.wobble) * 1.2;
enemy.x = clamp(enemy.x, 0, WIDTH - enemy.width);
});
for (let i = enemies.length - 1; i >= 0; i--) {
const enemy = enemies[i];
if (rectsOverlap(player, enemy)) {
gameOver = true;
break;
}
let destroyed = false;
for (let j = bullets.length - 1; j >= 0; j--) {
if (rectsOverlap(bullets[j], enemy)) {
bullets.splice(j, 1);
enemies.splice(i, 1);
score += 10;
destroyed = true;
break;
}
}
if (destroyed) continue;
}
enemies = enemies.filter((enemy) => enemy.y < HEIGHT + enemy.height);
}
function updateStars() {
for (const star of stars) {
star.y += star.speed;
if (star.y > HEIGHT) {
star.y = -star.size;
star.x = Math.random() * WIDTH;
star.size = Math.random() * 2 + 1;
star.speed = Math.random() * 1.4 + 0.6;
}
}
}
function drawBackground() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (const star of stars) {
ctx.fillStyle = '#ffffff';
ctx.fillRect(star.x, star.y, star.size, star.size);
}
}
function drawPlayer(p) {
const x = p.x;
const y = p.y;
// wings
ctx.fillStyle = '#7c3aed';
ctx.beginPath();
ctx.moveTo(x + 8, y + 22);
ctx.lineTo(x - 4, y + 40);
ctx.lineTo(x + 10, y + 36);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + p.width - 8, y + 22);
ctx.lineTo(x + p.width + 4, y + 40);
ctx.lineTo(x + p.width - 10, y + 36);
ctx.closePath();
ctx.fill();
// body
ctx.fillStyle = '#22c55e';
ctx.beginPath();
ctx.moveTo(x + p.width / 2, y);
ctx.lineTo(x + p.width - 2, y + 18);
ctx.lineTo(x + p.width - 7, y + 42);
ctx.lineTo(x + p.width / 2, y + p.height);
ctx.lineTo(x + 7, y + 42);
ctx.lineTo(x + 2, y + 18);
ctx.closePath();
ctx.fill();
// belly
ctx.fillStyle = '#fde68a';
ctx.beginPath();
ctx.moveTo(x + p.width / 2, y + 10);
ctx.lineTo(x + p.width - 11, y + 28);
ctx.lineTo(x + p.width / 2, y + p.height - 6);
ctx.lineTo(x + 11, y + 28);
ctx.closePath();
ctx.fill();
// horns
ctx.fillStyle = '#f59e0b';
ctx.fillRect(x + 10, y + 6, 4, 8);
ctx.fillRect(x + p.width - 14, y + 6, 4, 8);
// eyes
ctx.fillStyle = '#111827';
ctx.fillRect(x + 14, y + 18, 4, 4);
ctx.fillRect(x + p.width - 18, y + 18, 4, 4);
// fire tail glow
ctx.fillStyle = '#fb923c';
ctx.beginPath();
ctx.moveTo(x + p.width / 2, y + p.height);
ctx.lineTo(x + p.width / 2 - 6, y + p.height + 14);
ctx.lineTo(x + p.width / 2 + 6, y + p.height + 14);
ctx.closePath();
ctx.fill();
}
function drawBullets() {
bullets.forEach((bullet) => {
ctx.fillStyle = '#f97316';
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
ctx.fillStyle = '#fde68a';
ctx.fillRect(bullet.x + 2, bullet.y - 4, bullet.width - 4, 6);
});
}
function drawEnemy(enemy) {
const x = enemy.x;
const y = enemy.y;
const w = enemy.width;
const h = enemy.height;
ctx.fillStyle = '#ef4444';
ctx.beginPath();
ctx.moveTo(x + w / 2, y);
ctx.lineTo(x + w, y + h * 0.4);
ctx.lineTo(x + w * 0.8, y + h);
ctx.lineTo(x + w * 0.2, y + h);
ctx.lineTo(x, y + h * 0.4);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#fecaca';
ctx.fillRect(x + w * 0.28, y + h * 0.34, w * 0.14, h * 0.14);
ctx.fillRect(x + w * 0.58, y + h * 0.34, w * 0.14, h * 0.14);
ctx.fillStyle = '#7f1d1d';
ctx.fillRect(x + w * 0.42, y + h * 0.54, w * 0.16, h * 0.12);
}
function drawHUD() {
ctx.fillStyle = 'rgba(15, 23, 42, 0.7)';
ctx.fillRect(12, 12, 130, 42);
ctx.strokeStyle = '#7dd3fc';
ctx.strokeRect(12, 12, 130, 42);
ctx.fillStyle = '#e2e8f0';
ctx.font = 'bold 22px Arial';
ctx.fillText('Score: ' + score, 24, 40);
}
function drawGameOver() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
ctx.fillStyle = '#fff';
ctx.textAlign = 'center';
ctx.font = 'bold 42px Arial';
ctx.fillText('GAME OVER', WIDTH / 2, HEIGHT / 2 - 24);
ctx.font = '24px Arial';
ctx.fillText('最終分數:' + score, WIDTH / 2, HEIGHT / 2 + 18);
ctx.font = '18px Arial';
ctx.fillText('按下「重新開始」再玩一次', WIDTH / 2, HEIGHT / 2 + 58);
ctx.textAlign = 'start';
}
function draw() {
drawBackground();
drawHUD();
drawBullets();
enemies.forEach(drawEnemy);
drawPlayer(player);
if (gameOver) {
drawGameOver();
}
}
function gameLoop() {
update();
draw();
animationId = requestAnimationFrame(gameLoop);
}
window.addEventListener('keydown', (e) => {
if (e.code === 'ArrowLeft' || e.code === 'ArrowRight' || e.code === 'Space') {
e.preventDefault();
}
if (e.code in keys) {
keys[e.code] = true;
}
});
window.addEventListener('keyup', (e) => {
if (e.code in keys) {
keys[e.code] = false;
}
});
restartBtn.addEventListener('click', () => {
resetGame();
});
resetGame();
cancelAnimationFrame(animationId);
gameLoop();
</script>
</body>
</html>
有 2 位朋友喜欢这篇文章:shiningdracon, 闪耀
←目前頭像感謝安雅贈圖。

Quit, dont quit... Noodles, dont noodles...
There is a saying: yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.
离线
參考貪喫蛇遊戲


<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>東方龍貪吃蛇</title>
<style>
:root {
--bg1: #0f172a;
--bg2: #1e293b;
--panel: rgba(15, 23, 42, 0.82);
--gold: #fbbf24;
--gold2: #fde68a;
--jade: #34d399;
--danger: #fb7185;
--text: #e2e8f0;
--muted: #94a3b8;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: "Segoe UI", "Noto Sans TC", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top, rgba(251, 191, 36, 0.18), transparent 28%),
radial-gradient(circle at bottom right, rgba(52, 211, 153, 0.14), transparent 26%),
linear-gradient(180deg, var(--bg2), var(--bg1));
padding: 20px;
}
.wrap {
width: min(92vw, 760px);
background: var(--panel);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 24px;
box-shadow: 0 24px 60px rgba(0,0,0,0.35);
backdrop-filter: blur(8px);
padding: 20px;
}
.topbar {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.title h1 {
margin: 0;
font-size: clamp(26px, 4vw, 36px);
color: var(--gold2);
letter-spacing: 1px;
}
.title p {
margin: 6px 0 0;
color: var(--muted);
font-size: 14px;
}
.stats {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.chip {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 999px;
padding: 10px 14px;
font-weight: 700;
font-size: 14px;
}
.chip span {
color: var(--gold2);
margin-left: 6px;
}
.game-box {
position: relative;
display: grid;
place-items: center;
}
canvas {
width: min(92vw, 640px);
height: min(92vw, 640px);
max-width: 640px;
max-height: 640px;
aspect-ratio: 1 / 1;
background:
linear-gradient(180deg, rgba(15, 23, 42, 0.94), rgba(2, 6, 23, 0.96));
border-radius: 20px;
border: 2px solid rgba(251, 191, 36, 0.25);
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.35);
}
.overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.panel {
pointer-events: auto;
text-align: center;
width: min(84%, 420px);
padding: 24px 22px;
border-radius: 22px;
background: rgba(15, 23, 42, 0.9);
border: 1px solid rgba(255,255,255,0.08);
box-shadow: 0 16px 40px rgba(0,0,0,0.4);
}
.panel h2 {
margin: 0 0 10px;
color: var(--gold2);
font-size: 28px;
}
.panel p {
margin: 8px 0;
color: var(--text);
line-height: 1.6;
}
.actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-top: 16px;
}
button {
border: none;
border-radius: 999px;
padding: 12px 20px;
font-size: 15px;
font-weight: 800;
cursor: pointer;
color: #0f172a;
background: linear-gradient(135deg, var(--gold2), var(--gold));
box-shadow: 0 10px 24px rgba(251, 191, 36, 0.22);
transition: transform 0.15s ease, filter 0.15s ease;
}
button:hover {
transform: translateY(-1px);
filter: brightness(1.03);
}
.ghost {
background: rgba(255,255,255,0.08);
color: var(--text);
box-shadow: none;
border: 1px solid rgba(255,255,255,0.08);
}
.footer {
margin-top: 14px;
color: var(--muted);
font-size: 13px;
text-align: center;
line-height: 1.6;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div class="title">
<h1>東方龍貪吃蛇</h1>
<p>以方向鍵或 WASD 操控龍身,吃靈珠變長,別撞到自己或牆壁。</p>
</div>
<div class="stats">
<div class="chip">分數 <span id="score">0</span></div>
<div class="chip">最高分 <span id="best">0</span></div>
<div class="chip">速度 <span id="speed">1</span></div>
</div>
</div>
<div class="game-box">
<canvas id="game" width="640" height="640"></canvas>
<div class="overlay" id="startOverlay">
<div class="panel">
<h2>開始遊戲</h2>
<p>你將操控一條東方龍在夜空中穿梭,吞食靈珠成長。</p>
<p>操作:方向鍵 / WASD</p>
<div class="actions">
<button id="startBtn">開始</button>
</div>
</div>
</div>
<div class="overlay hidden" id="gameOverOverlay">
<div class="panel">
<h2>遊戲結束</h2>
<p>本次分數:<strong id="finalScore">0</strong></p>
<p id="finalMessage">龍撞上障礙了,再試一次。</p>
<div class="actions">
<button id="restartBtn">重新開始</button>
<button class="ghost" id="closeBtn">查看畫面</button>
</div>
</div>
</div>
</div>
<div class="footer">
小提醒:每吃 4 顆靈珠,速度會提升一級。<br />
直接把這個 HTML 存成檔案後,用瀏覽器打開即可遊玩。
</div>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const scoreEl = document.getElementById('score');
const bestEl = document.getElementById('best');
const speedEl = document.getElementById('speed');
const startOverlay = document.getElementById('startOverlay');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const finalScoreEl = document.getElementById('finalScore');
const finalMessageEl = document.getElementById('finalMessage');
const startBtn = document.getElementById('startBtn');
const restartBtn = document.getElementById('restartBtn');
const closeBtn = document.getElementById('closeBtn');
const gridCount = 20;
const cell = canvas.width / gridCount;
let dragon = [];
let direction = { x: 1, y: 0 };
let nextDirection = { x: 1, y: 0 };
let orb = { x: 10, y: 10 };
let score = 0;
let best = Number(localStorage.getItem('dragonSnakeBest') || 0);
let level = 1;
let gameOver = false;
let started = false;
let frameTimer = 0;
let stepMs = 160;
let lastTime = 0;
bestEl.textContent = best;
function resetGame() {
dragon = [
{ x: 5, y: 10 },
{ x: 4, y: 10 },
{ x: 3, y: 10 },
{ x: 2, y: 10 }
];
direction = { x: 1, y: 0 };
nextDirection = { x: 1, y: 0 };
score = 0;
level = 1;
stepMs = 160;
gameOver = false;
scoreEl.textContent = score;
speedEl.textContent = level;
spawnOrb();
draw();
}
function spawnOrb() {
let valid = false;
while (!valid) {
orb = {
x: Math.floor(Math.random() * gridCount),
y: Math.floor(Math.random() * gridCount)
};
valid = !dragon.some(part => part.x === orb.x && part.y === orb.y);
}
}
function startGame() {
resetGame();
started = true;
startOverlay.classList.add('hidden');
gameOverOverlay.classList.add('hidden');
lastTime = performance.now();
frameTimer = 0;
requestAnimationFrame(loop);
}
function endGame(reason) {
gameOver = true;
started = false;
if (score > best) {
best = score;
localStorage.setItem('dragonSnakeBest', String(best));
bestEl.textContent = best;
}
finalScoreEl.textContent = score;
finalMessageEl.textContent = reason;
gameOverOverlay.classList.remove('hidden');
}
function update() {
direction = nextDirection;
const head = { ...dragon[0] };
const newHead = {
x: head.x + direction.x,
y: head.y + direction.y
};
if (
newHead.x < 0 || newHead.x >= gridCount ||
newHead.y < 0 || newHead.y >= gridCount
) {
endGame('東方龍撞上邊界了。');
return;
}
if (dragon.some(part => part.x === newHead.x && part.y === newHead.y)) {
endGame('東方龍撞到自己的龍身了。');
return;
}
dragon.unshift(newHead);
if (newHead.x === orb.x && newHead.y === orb.y) {
score += 10;
scoreEl.textContent = score;
if ((score / 10) % 4 === 0) {
level += 1;
speedEl.textContent = level;
stepMs = Math.max(70, stepMs - 12);
}
spawnOrb();
} else {
dragon.pop();
}
}
function drawBackground() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const bg = ctx.createLinearGradient(0, 0, 0, canvas.height);
bg.addColorStop(0, '#0f172a');
bg.addColorStop(1, '#020617');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < gridCount; y++) {
for (let x = 0; x < gridCount; x++) {
ctx.fillStyle = (x + y) % 2 === 0 ? 'rgba(255,255,255,0.025)' : 'rgba(255,255,255,0.012)';
ctx.fillRect(x * cell, y * cell, cell, cell);
}
}
for (let i = 0; i < 24; i++) {
const sx = (i * 97) % canvas.width;
const sy = (i * 53) % canvas.height;
ctx.fillStyle = 'rgba(255,255,255,0.18)';
ctx.beginPath();
ctx.arc(sx, sy, 1.4, 0, Math.PI * 2);
ctx.fill();
}
}
function drawOrb() {
const cx = orb.x * cell + cell / 2;
const cy = orb.y * cell + cell / 2;
const glow = ctx.createRadialGradient(cx, cy, 2, cx, cy, cell * 0.45);
glow.addColorStop(0, 'rgba(253, 224, 71, 0.95)');
glow.addColorStop(1, 'rgba(253, 224, 71, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(cx, cy, cell * 0.48, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fde68a';
ctx.beginPath();
ctx.arc(cx, cy, cell * 0.22, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 2;
ctx.stroke();
}
function drawDragon() {
dragon.forEach((part, index) => {
const px = part.x * cell;
const py = part.y * cell;
const isHead = index === 0;
if (isHead) {
const headGrad = ctx.createLinearGradient(px, py, px + cell, py + cell);
headGrad.addColorStop(0, '#fde68a');
headGrad.addColorStop(1, '#f59e0b');
ctx.fillStyle = headGrad;
roundRect(px + 3, py + 3, cell - 6, cell - 6, 10, true, false);
ctx.fillStyle = '#7c2d12';
const eyeOffsetX = direction.x === -1 ? 9 : direction.x === 1 ? cell - 13 : 10;
const eye2OffsetX = direction.x === -1 ? 9 : direction.x === 1 ? cell - 13 : cell - 14;
const eyeY = direction.y === 1 ? cell - 13 : 11;
ctx.beginPath();
ctx.arc(px + eyeOffsetX, py + eyeY, 2.3, 0, Math.PI * 2);
ctx.arc(px + eye2OffsetX, py + eyeY, 2.3, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 2;
ctx.beginPath();
if (direction.x === 1) {
ctx.moveTo(px + cell - 4, py + cell / 2);
ctx.lineTo(px + cell + 6, py + cell / 2 - 3);
ctx.lineTo(px + cell + 9, py + cell / 2);
ctx.lineTo(px + cell + 6, py + cell / 2 + 3);
} else if (direction.x === -1) {
ctx.moveTo(px + 4, py + cell / 2);
ctx.lineTo(px - 6, py + cell / 2 - 3);
ctx.lineTo(px - 9, py + cell / 2);
ctx.lineTo(px - 6, py + cell / 2 + 3);
} else if (direction.y === -1) {
ctx.moveTo(px + cell / 2, py + 4);
ctx.lineTo(px + cell / 2 - 3, py - 6);
ctx.lineTo(px + cell / 2, py - 9);
ctx.lineTo(px + cell / 2 + 3, py - 6);
} else {
ctx.moveTo(px + cell / 2, py + cell - 4);
ctx.lineTo(px + cell / 2 - 3, py + cell + 6);
ctx.lineTo(px + cell / 2, py + cell + 9);
ctx.lineTo(px + cell / 2 + 3, py + cell + 6);
}
ctx.stroke();
ctx.strokeStyle = '#fef3c7';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(px + 8, py + 7);
ctx.lineTo(px + 11, py + 2);
ctx.moveTo(px + cell - 8, py + 7);
ctx.lineTo(px + cell - 11, py + 2);
ctx.stroke();
} else {
const t = index / dragon.length;
const bodyGrad = ctx.createLinearGradient(px, py, px + cell, py + cell);
bodyGrad.addColorStop(0, `rgba(52, 211, 153, ${0.95 - t * 0.25})`);
bodyGrad.addColorStop(1, `rgba(16, 185, 129, ${0.9 - t * 0.25})`);
ctx.fillStyle = bodyGrad;
roundRect(px + 4, py + 4, cell - 8, cell - 8, 9, true, false);
ctx.strokeStyle = 'rgba(253, 230, 138, 0.55)';
ctx.lineWidth = 1.6;
ctx.beginPath();
ctx.arc(px + cell / 2, py + cell / 2, cell * 0.18, Math.PI * 0.2, Math.PI * 1.8);
ctx.stroke();
}
});
}
function roundRect(x, y, w, h, r, fill, stroke) {
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();
if (fill) ctx.fill();
if (stroke) ctx.stroke();
}
function draw() {
drawBackground();
drawOrb();
drawDragon();
}
function loop(timestamp) {
if (!started) return;
const delta = timestamp - lastTime;
lastTime = timestamp;
frameTimer += delta;
while (frameTimer >= stepMs && !gameOver) {
update();
frameTimer -= stepMs;
}
draw();
if (!gameOver) {
requestAnimationFrame(loop);
}
}
function setDirection(x, y) {
if (!started && startOverlay.classList.contains('hidden') === false) return;
if (direction.x === -x && direction.y === -y) return;
nextDirection = { x, y };
}
document.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (["arrowup", "arrowdown", "arrowleft", "arrowright", "w", "a", "s", "d", " "].includes(key)) {
e.preventDefault();
}
if (!started && (key === ' ' || key === 'enter')) {
startGame();
return;
}
if (key === 'arrowup' || key === 'w') setDirection(0, -1);
else if (key === 'arrowdown' || key === 's') setDirection(0, 1);
else if (key === 'arrowleft' || key === 'a') setDirection(-1, 0);
else if (key === 'arrowright' || key === 'd') setDirection(1, 0);
});
startBtn.addEventListener('click', startGame);
restartBtn.addEventListener('click', startGame);
closeBtn.addEventListener('click', () => gameOverOverlay.classList.add('hidden'));
resetGame();
</script>
</body>
</html>
←目前頭像感謝安雅贈圖。

Quit, dont quit... Noodles, dont noodles...
There is a saying: yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.
离线
翻牌配對遊戲

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>奇幻 ANSI 翻牌配對遊戲</title>
<style>
:root {
--bg: #0a0f0a;
--panel: #101810;
--border: #33ff66;
--text: #b7ffbf;
--dim: #6fcf7f;
--accent: #00ffaa;
--warn: #ffee55;
--match: #66ff99;
--danger: #ff6688;
--shadow: rgba(0, 255, 120, 0.18);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top, #112211 0%, #081008 45%, #040804 100%);
color: var(--text);
font-family: "Courier New", Consolas, monospace;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.game-shell {
width: min(920px, 100%);
background: var(--panel);
border: 3px solid var(--border);
box-shadow:
0 0 20px var(--shadow),
inset 0 0 12px rgba(0,255,120,0.08);
padding: 20px;
border-radius: 10px;
}
.title {
white-space: pre-wrap;
line-height: 1.1;
color: var(--accent);
font-size: 14px;
margin: 0 0 14px 0;
text-shadow: 0 0 6px rgba(0,255,180,0.35);
}
.hud {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.stats {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.stat-box {
border: 2px solid var(--border);
padding: 8px 12px;
min-width: 120px;
background: rgba(0, 40, 0, 0.45);
box-shadow: inset 0 0 8px rgba(0,255,100,0.08);
}
.stat-label {
color: var(--dim);
font-size: 12px;
margin-bottom: 4px;
}
.stat-value {
color: var(--warn);
font-weight: bold;
font-size: 20px;
}
button {
background: #081108;
color: var(--text);
border: 2px solid var(--border);
padding: 10px 16px;
font-family: inherit;
font-size: 15px;
cursor: pointer;
border-radius: 6px;
transition: 0.15s ease;
box-shadow: 0 0 10px rgba(0,255,120,0.08);
}
button:hover {
background: #102010;
color: #ffffff;
transform: translateY(-1px);
}
.message {
min-height: 28px;
margin-bottom: 14px;
color: var(--accent);
font-size: 15px;
}
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.card {
aspect-ratio: 3 / 4;
perspective: 1000px;
cursor: pointer;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.45s ease;
transform-style: preserve-3d;
}
.card.flipped .card-inner,
.card.matched .card-inner {
transform: rotateY(180deg);
}
.card-face {
position: absolute;
inset: 0;
border: 2px solid var(--border);
border-radius: 8px;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
overflow: hidden;
box-shadow:
inset 0 0 12px rgba(0,255,120,0.08),
0 0 10px rgba(0,255,120,0.08);
}
.card-back {
background:
linear-gradient(135deg, rgba(0,255,120,0.07), rgba(0,255,255,0.03)),
#071007;
color: var(--accent);
font-size: 16px;
text-align: center;
white-space: pre-wrap;
}
.card-front {
background:
linear-gradient(180deg, rgba(30,50,30,0.7), rgba(8,16,8,0.95)),
#0b140b;
transform: rotateY(180deg);
color: var(--text);
}
.ansi-art {
white-space: pre;
line-height: 1.02;
font-size: clamp(8px, 1.1vw, 12px);
text-align: left;
user-select: none;
text-shadow: 0 0 4px rgba(0,255,120,0.18);
}
.card.matched .card-face {
border-color: var(--match);
box-shadow:
inset 0 0 14px rgba(120,255,160,0.12),
0 0 14px rgba(120,255,160,0.22);
}
.footer {
margin-top: 14px;
color: var(--dim);
font-size: 13px;
line-height: 1.6;
}
.blink {
animation: blink 1s step-start infinite;
}
@keyframes blink {
50% { opacity: 0.2; }
}
@media (max-width: 700px) {
.board {
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.game-shell {
padding: 14px;
}
.title {
font-size: 11px;
}
.stat-box {
min-width: 96px;
}
}
</style>
</head>
<body>
<div class="game-shell">
<pre class="title">
╔══════════════════════════════════════════════╗
║ FANTASY ANSI MEMORY MATCH : DRAGON QUEST ║
╚══════════════════════════════════════════════╝
</pre>
<div class="hud">
<div class="stats">
<div class="stat-box">
<div class="stat-label">TIME</div>
<div class="stat-value" id="timer">0 秒</div>
</div>
<div class="stat-box">
<div class="stat-label">MATCHED</div>
<div class="stat-value" id="matchedCount">0 / 8</div>
</div>
<div class="stat-box">
<div class="stat-label">TURNS</div>
<div class="stat-value" id="turnCount">0</div>
</div>
</div>
<button id="restartBtn">重新開始</button>
</div>
<div class="message" id="message">請翻開兩張牌,找出相同的奇幻角色。</div>
<div class="board" id="board"></div>
<div class="footer">
操作方式:用滑鼠點擊卡牌翻面。<span class="blink">█</span><br>
主題:奇幻世界、Dragon、ANSI 復古終端機風格
</div>
</div>
<script>
const cardDesigns = [
{
name: "DRAGON",
art: String.raw`
/\_/\
/ o o \
( " )__
/| ___ |\ \
/_|/ \|_\_/
/^^^\
/_/ \_\
DRAGON
`
},
{
name: "WIZARD",
art: String.raw`
/\
/__\
\ /
/==\
/====\
||
\_==_/
/ \
WIZARD
`
},
{
name: "KNIGHT",
art: String.raw`
O
/|\
/ \
|=+=|
|===|
|===|
/ \
/___\
KNIGHT
`
},
{
name: "SLIME",
art: String.raw`
______
/ \
/ o o \
| -- |
\ ____ /
\______/
SLIME
`
},
{
name: "CHEST",
art: String.raw`
__________
/_________/|
| _ _ | |
| | | | || |
| |_| |_|| |
| ___ | /
|_/___\__|/
CHEST
`
},
{
name: "PHOENIX",
art: String.raw`
\ | /
\ | /
---\|/---
/^^\
/_/\_\
/ || \
/ || \
PHOENIX
`
},
{
name: "SWORD",
art: String.raw`
/\
||
||
||
||
==||==
||
||
/__\
SWORD
`
},
{
name: "CASTLE",
art: String.raw`
|>>> <<<|
|[]| |[]|
| |__| |
_| ____ |_
| | | | | |
|_|_|____|_|_|
CASTLE
`
}
];
const board = document.getElementById("board");
const timerEl = document.getElementById("timer");
const matchedCountEl = document.getElementById("matchedCount");
const turnCountEl = document.getElementById("turnCount");
const messageEl = document.getElementById("message");
const restartBtn = document.getElementById("restartBtn");
let cards = [];
let firstCard = null;
let secondCard = null;
let lockBoard = false;
let matchedPairs = 0;
let turns = 0;
let seconds = 0;
let timer = null;
let started = false;
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function startTimer() {
if (timer) return;
timer = setInterval(() => {
seconds++;
timerEl.textContent = `${seconds} 秒`;
}, 1000);
}
function stopTimer() {
clearInterval(timer);
timer = null;
}
function updateStats() {
matchedCountEl.textContent = `${matchedPairs} / 8`;
turnCountEl.textContent = turns;
}
function setMessage(text, color = "var(--accent)") {
messageEl.textContent = text;
messageEl.style.color = color;
}
function createCardElement(cardData, index) {
const card = document.createElement("div");
card.className = "card";
card.dataset.name = cardData.name;
card.dataset.index = index;
card.innerHTML = `
<div class="card-inner">
<div class="card-face card-back">
<pre class="ansi-art">+--------+
| ###### |
| ##??## |
| ###### |
| FANTSY |
+--------+</pre>
</div>
<div class="card-face card-front">
<pre class="ansi-art">${cardData.art}</pre>
</div>
</div>
`;
card.addEventListener("click", onCardClick);
return card;
}
function buildBoard() {
board.innerHTML = "";
const doubled = [...cardDesigns, ...cardDesigns].map((c, i) => ({
...c,
uid: i + "-" + Math.random().toString(16).slice(2)
}));
cards = shuffle(doubled);
cards.forEach((cardData, index) => {
const el = createCardElement(cardData, index);
board.appendChild(el);
});
}
function resetTurnState() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
function onCardClick(e) {
const clicked = e.currentTarget;
if (lockBoard) return;
if (clicked === firstCard) return;
if (clicked.classList.contains("flipped")) return;
if (clicked.classList.contains("matched")) return;
if (!started) {
started = true;
startTimer();
}
clicked.classList.add("flipped");
if (!firstCard) {
firstCard = clicked;
setMessage("已翻開第 1 張牌…");
return;
}
secondCard = clicked;
lockBoard = true;
turns++;
updateStats();
const isMatch = firstCard.dataset.name === secondCard.dataset.name;
if (isMatch) {
firstCard.classList.add("matched");
secondCard.classList.add("matched");
matchedPairs++;
updateStats();
if (matchedPairs === 8) {
stopTimer();
setMessage(`恭喜完成!總共 ${seconds} 秒,翻牌 ${turns} 次。`, "var(--match)");
} else {
setMessage(`配對成功:${firstCard.dataset.name}`, "var(--match)");
}
resetTurnState();
} else {
setMessage("沒有配對成功,再試一次。", "var(--danger)");
setTimeout(() => {
firstCard.classList.remove("flipped");
secondCard.classList.remove("flipped");
resetTurnState();
}, 850);
}
}
function resetGame() {
stopTimer();
firstCard = null;
secondCard = null;
lockBoard = false;
matchedPairs = 0;
turns = 0;
seconds = 0;
started = false;
timerEl.textContent = "0 秒";
updateStats();
setMessage("請翻開兩張牌,找出相同的奇幻角色。");
buildBoard();
}
restartBtn.addEventListener("click", resetGame);
resetGame();
</script>
</body>
</html>
←目前頭像感謝安雅贈圖。

Quit, dont quit... Noodles, dont noodles...
There is a saying: yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.
离线
要不要試試開一個 GitHub Pages?這樣就能直接在線玩了
這東西挺適合這種純前端小遊戲的,不用自己搭服務器,也不需要額外配置環境,把代碼丟上去就能跑。
而且免費,自帶版本管理,後續更新也很方便,改完代碼再 push 一下就更新了。
我之前和別人一起做過一個詭法棋遊戲,前陣子翻出來順手改成了棋譜網站,也放在 GitHub Pages 上了:https://shiningdracon.github.io/guifa/
有 1 位朋友喜欢这篇文章:龍爪翻書
以龍為本
<-- 目前頭像 by 理業化肥
聯繫方式:站內短消息或郵件
离线
原本想順便養龍蝦,叫龍蝦幫忙發布到GitHub,發現沒想像中好弄
還是先繼續貼純文字過來吧
像素風格、橫版通關遊戲

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>像素小龍橫版通關</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #111827;
color: #e5e7eb;
font-family: "Noto Sans TC", system-ui, sans-serif;
user-select: none;
}
.wrap {
width: min(960px, 100vw);
padding: 12px;
}
h1 {
margin: 0 0 8px;
font-size: 20px;
letter-spacing: 2px;
}
.hud {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 8px;
font-size: 14px;
opacity: 0.95;
}
canvas {
width: 100%;
aspect-ratio: 16 / 9;
display: block;
border: 4px solid #374151;
border-radius: 10px;
background: #7dd3fc;
image-rendering: pixelated;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.35);
}
.help {
margin-top: 8px;
font-size: 14px;
line-height: 1.6;
color: #cbd5e1;
}
kbd {
padding: 2px 6px;
border-radius: 4px;
background: #334155;
color: white;
border-bottom: 2px solid #0f172a;
}
</style>
</head>
<body>
<div class="wrap">
<h1>🐉 像素小龍:火焰通關</h1>
<div class="hud">
<div id="status">生命:3 分數:0</div>
<div id="message">飛到右方城堡即可通關!</div>
</div>
<canvas id="game" width="640" height="360"></canvas>
<div class="help">
操作:<kbd>W</kbd>/<kbd>↑</kbd> 上飛,<kbd>S</kbd>/<kbd>↓</kbd> 下飛,<kbd>A</kbd>/<kbd>←</kbd> 左移,<kbd>D</kbd>/<kbd>→</kbd> 右移,<kbd>空白鍵</kbd> 噴火球,<kbd>R</kbd> 重新開始。
</div>
</div>
<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("status");
const messageEl = document.getElementById("message");
const W = canvas.width;
const H = canvas.height;
const WORLD_W = 3600;
const keys = new Set();
let cameraX = 0;
let frame = 0;
let gameState = "play";
const dragonStart = { x: 70, y: 160 };
const dragon = {
x: dragonStart.x,
y: dragonStart.y,
w: 34,
h: 24,
vx: 0,
vy: 0,
hp: 3,
inv: 0,
fireCd: 0,
score: 0
};
let fireballs = [];
let enemies = [];
let particles = [];
let clouds = [];
function resetGame() {
cameraX = 0;
frame = 0;
gameState = "play";
Object.assign(dragon, {
x: dragonStart.x, y: dragonStart.y, vx: 0, vy: 0,
hp: 3, inv: 0, fireCd: 0, score: 0
});
fireballs = [];
particles = [];
enemies = [];
clouds = [];
for (let i = 0; i < 18; i++) {
clouds.push({
x: i * 220 + Math.random() * 90,
y: 30 + Math.random() * 95,
s: 1 + Math.random() * 1.4
});
}
for (let i = 0; i < 24; i++) {
enemies.push({
x: 380 + i * 130 + Math.random() * 60,
y: 70 + Math.random() * 190,
w: 24,
h: 22,
baseY: 70 + Math.random() * 190,
hp: i % 5 === 0 ? 2 : 1,
type: i % 3,
alive: true,
phase: Math.random() * Math.PI * 2
});
}
updateHud();
messageEl.textContent = "飛到右方城堡即可通關!";
}
function updateHud() {
statusEl.textContent = `生命:${dragon.hp} 分數:${dragon.score}`;
}
function rectsHit(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 pixelRect(x, y, w, h, color) {
ctx.fillStyle = color;
ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
}
function drawCloud(c) {
const x = c.x - cameraX * 0.35;
const y = c.y;
pixelRect(x, y + 8 * c.s, 34 * c.s, 10 * c.s, "#f8fafc");
pixelRect(x + 8 * c.s, y, 18 * c.s, 18 * c.s, "#f8fafc");
pixelRect(x + 24 * c.s, y + 4 * c.s, 20 * c.s, 14 * c.s, "#e0f2fe");
}
function drawBackground() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, "#38bdf8");
grad.addColorStop(0.65, "#bae6fd");
grad.addColorStop(1, "#86efac");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
clouds.forEach(drawCloud);
ctx.fillStyle = "#60a5fa";
for (let x = -120; x < W + 160; x += 140) {
const mx = x - (cameraX * 0.15 % 140);
ctx.beginPath();
ctx.moveTo(mx, 265);
ctx.lineTo(mx + 75, 165);
ctx.lineTo(mx + 160, 265);
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = "#22c55e";
ctx.fillRect(0, 314, W, 46);
ctx.fillStyle = "#15803d";
for (let x = -20; x < W + 20; x += 18) {
pixelRect(x - (cameraX % 18), 314, 9, 8, "#16a34a");
}
}
function drawDragon() {
const x = dragon.x - cameraX;
const y = dragon.y;
const blink = dragon.inv > 0 && Math.floor(frame / 5) % 2 === 0;
if (blink) return;
const flap = Math.floor(frame / 8) % 2;
pixelRect(x + 4, y + 9, 24, 12, "#22c55e");
pixelRect(x + 24, y + 6, 10, 10, "#16a34a");
pixelRect(x + 31, y + 9, 6, 5, "#15803d");
pixelRect(x + 27, y + 8, 3, 3, "#f8fafc");
pixelRect(x + 28, y + 9, 2, 2, "#111827");
pixelRect(x - 3, y + 13, 10, 6, "#15803d");
pixelRect(x + 8, y + (flap ? 1 : -5), 14, 8, "#86efac");
pixelRect(x + 10, y + (flap ? 3 : -3), 10, 4, "#bbf7d0");
pixelRect(x + 12, y + 21, 4, 5, "#166534");
pixelRect(x + 22, y + 21, 4, 5, "#166534");
}
function drawEnemy(e) {
const x = e.x - cameraX;
const y = e.y;
if (e.type === 0) {
pixelRect(x + 3, y + 5, 18, 12, "#7c2d12");
pixelRect(x, y + 8, 6, 6, "#f97316");
pixelRect(x + 19, y + 8, 6, 6, "#f97316");
pixelRect(x + 8, y + 2, 8, 4, "#fed7aa");
} else if (e.type === 1) {
pixelRect(x + 3, y + 4, 18, 14, "#6d28d9");
pixelRect(x + 7, y, 4, 5, "#a78bfa");
pixelRect(x + 15, y, 4, 5, "#a78bfa");
pixelRect(x + 9, y + 9, 3, 3, "#f8fafc");
} else {
pixelRect(x + 4, y + 7, 16, 10, "#0f766e");
pixelRect(x, y + 4, 7, 5, "#5eead4");
pixelRect(x + 18, y + 4, 7, 5, "#5eead4");
pixelRect(x + 9, y + 3, 6, 4, "#ccfbf1");
}
}
function drawCastle() {
const x = WORLD_W - 180 - cameraX;
const y = 232;
pixelRect(x, y, 112, 82, "#64748b");
pixelRect(x + 12, y - 42, 28, 42, "#475569");
pixelRect(x + 72, y - 56, 28, 56, "#475569");
pixelRect(x + 18, y - 58, 16, 16, "#ef4444");
pixelRect(x + 78, y - 72, 16, 16, "#ef4444");
pixelRect(x + 45, y + 35, 24, 47, "#1e293b");
for (let i = 0; i < 5; i++) pixelRect(x + 8 + i * 20, y + 14, 10, 12, "#94a3b8");
pixelRect(x - 18, y + 70, 148, 12, "#334155");
}
function shootFire() {
if (dragon.fireCd > 0 || gameState !== "play") return;
fireballs.push({
x: dragon.x + dragon.w - 2,
y: dragon.y + 11,
w: 14,
h: 8,
vx: 7.5,
life: 90
});
dragon.fireCd = 14;
for (let i = 0; i < 5; i++) {
particles.push({ x: dragon.x + 34, y: dragon.y + 14, vx: Math.random() * 2, vy: Math.random() * 2 - 1, life: 12 });
}
}
function damageDragon() {
if (dragon.inv > 0 || gameState !== "play") return;
dragon.hp--;
dragon.inv = 80;
updateHud();
messageEl.textContent = "小龍受傷了!小心敵人。";
if (dragon.hp <= 0) {
gameState = "lose";
messageEl.textContent = "挑戰失敗,按 R 重新開始。";
}
}
function update() {
frame++;
if (keys.has("r") || keys.has("R")) resetGame();
if (gameState !== "play") return;
let ax = 0, ay = 0;
if (keys.has("ArrowRight") || keys.has("d") || keys.has("D")) ax += 0.45;
if (keys.has("ArrowLeft") || keys.has("a") || keys.has("A")) ax -= 0.45;
if (keys.has("ArrowUp") || keys.has("w") || keys.has("W")) ay -= 0.55;
if (keys.has("ArrowDown") || keys.has("s") || keys.has("S")) ay += 0.55;
if (keys.has(" ")) shootFire();
dragon.vx = (dragon.vx + ax) * 0.86;
dragon.vy = (dragon.vy + ay + 0.05) * 0.9;
dragon.x += dragon.vx;
dragon.y += dragon.vy;
dragon.x = Math.max(20, Math.min(WORLD_W - 220, dragon.x));
dragon.y = Math.max(18, Math.min(286, dragon.y));
dragon.fireCd = Math.max(0, dragon.fireCd - 1);
dragon.inv = Math.max(0, dragon.inv - 1);
cameraX = Math.max(0, Math.min(WORLD_W - W, dragon.x - 180));
fireballs.forEach(f => { f.x += f.vx; f.life--; });
fireballs = fireballs.filter(f => f.life > 0 && f.x < WORLD_W);
enemies.forEach(e => {
if (!e.alive) return;
e.y = e.baseY + Math.sin(frame * 0.035 + e.phase) * 32;
e.x -= 0.35 + e.type * 0.05;
if (e.x < cameraX - 80) e.x += 900;
if (rectsHit(dragon, e)) damageDragon();
});
for (const f of fireballs) {
for (const e of enemies) {
if (!e.alive) continue;
if (rectsHit(f, e)) {
f.life = 0;
e.hp--;
for (let i = 0; i < 10; i++) particles.push({ x: e.x + 12, y: e.y + 11, vx: Math.random() * 4 - 2, vy: Math.random() * 4 - 2, life: 18 });
if (e.hp <= 0) {
e.alive = false;
dragon.score += 100;
updateHud();
}
}
}
}
fireballs = fireballs.filter(f => f.life > 0);
particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life--; });
particles = particles.filter(p => p.life > 0);
if (dragon.x > WORLD_W - 260) {
gameState = "win";
dragon.score += 500;
updateHud();
messageEl.textContent = "通關成功!小龍抵達城堡。按 R 再玩一次。";
}
}
function drawFireball(f) {
const x = f.x - cameraX;
pixelRect(x, f.y, 14, 8, "#f97316");
pixelRect(x + 7, f.y + 2, 9, 4, "#fde047");
pixelRect(x - 5, f.y + 2, 6, 4, "#dc2626");
}
function drawOverlay() {
if (gameState === "play") return;
ctx.fillStyle = "rgba(15, 23, 42, 0.7)";
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = "#fff";
ctx.font = "24px monospace";
ctx.textAlign = "center";
ctx.fillText(gameState === "win" ? "CLEAR! 通關成功" : "GAME OVER", W / 2, H / 2 - 10);
ctx.font = "14px monospace";
ctx.fillText("按 R 重新開始", W / 2, H / 2 + 20);
ctx.textAlign = "left";
}
function draw() {
drawBackground();
drawCastle();
enemies.forEach(e => { if (e.alive) drawEnemy(e); });
fireballs.forEach(drawFireball);
particles.forEach(p => pixelRect(p.x - cameraX, p.y, 3, 3, p.life % 2 ? "#f97316" : "#fde047"));
drawDragon();
const progress = dragon.x / (WORLD_W - 260);
pixelRect(16, 16, 160, 8, "#1e293b");
pixelRect(18, 18, Math.max(0, Math.min(156, 156 * progress)), 4, "#facc15");
drawOverlay();
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
window.addEventListener("keydown", e => {
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(e.key)) e.preventDefault();
keys.add(e.key);
});
window.addEventListener("keyup", e => keys.delete(e.key));
resetGame();
loop();
</script>
</body>
</html>
←目前頭像感謝安雅贈圖。

Quit, dont quit... Noodles, dont noodles...
There is a saying: yesterday is history, tomorrow is a mystery, but today is a gift. That is why it is called the present.
离线