只需要安装 Pygame:
pip install pygame
把下面代码保存为 dnf.html,双击用 Chrome / Edge 打开即可。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DNF 简约版</title>
<style>
html,body{margin:0;height:100%;background:#0d0d12;display:flex;
align-items:center;justify-content:center;font-family:system-ui,sans-serif}
canvas{border:2px solid #333;border-radius:6px;box-shadow:0 0 30px #000}
</style>
</head>
<body>
<canvas id="game" width="960" height="540"></canvas>
<script>
(() => {
const cvs = document.getElementById('game');
const ctx = cvs.getContext('2d');
const W = cvs.width, H = cvs.height;
const GROUND = 440;
const GRAVITY = 0.8;
// ================= 输入 =================
const keys = {};
let jumpRequested = false;
const blockKeys = ' ', 'arrowup', 'arrowdown', 'arrowleft', 'arrowright';
addEventListener('keydown', e => {
const k = e.key.toLowerCase();
if (blockKeys.includes(k)) e.preventDefault();
if (keysk) return; // 忽略按住重复触发
keysk = true;
if (k === 'w' || k === 'arrowup' || k === ' ') jumpRequested = true;
if (k === 'j' || k === 'z') player && player.attack();
if (k === 'r' && state !== 'playing') reset();
});
addEventListener('keyup', e => { keyse.key.toLowerCase() = false; });
// ================= 碰撞 =================
const hit = (a, b) =>
a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
// ================= 角色基类 =================
class Fighter {
constructor(x, y, w, h, color, maxHp) {
this.x = x; this.y = y; this.w = w; this.h = h;
this.color = color;
this.maxHp = maxHp; this.hp = maxHp;
this.vy = 0; this.onGround = false;
this.facing = 1;
this.attackTimer = 0;
this.hitTimer = 0;
}
get box() { return { x: this.x, y: this.y, w: this.w, h: this.h }; }
gravity() {
this.vy += GRAVITY;
this.y += this.vy;
if (this.y + this.h >= GROUND) {
this.y = GROUND - this.h;
this.vy = 0;
this.onGround = true;
} else this.onGround = false;
}
attackBox() {
if (this.attackTimer <= 0) return null;
const aw = 50, ah = 50;
return {
x: this.facing === 1 ? this.x + this.w : this.x - aw,
y: this.y + this.h / 2 - ah / 2,
w: aw, h: ah
};
}
draw() {
ctx.fillStyle = this.hitTimer > 0 ? '#ffffff' : this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
// 血条
const r = Math.max(0, this.hp / this.maxHp);
ctx.fillStyle = '#333';
ctx.fillRect(this.x, this.y - 12, this.w, 6);
ctx.fillStyle = r > 0.4 ? '#4caf50' : '#e0a030';
ctx.fillRect(this.x, this.y - 12, this.w * r, 6);
// 攻击框提示
const ab = this.attackBox();
if (ab) {
ctx.strokeStyle = '#ffd54f';
ctx.lineWidth = 2;
ctx.strokeRect(ab.x, ab.y, ab.w, ab.h);
}
}
}
// ================= 玩家 =================
class Player extends Fighter {
constructor() {
super(120, GROUND - 70, 40, 70, '#4a8fe0', 100);
}
update() {
if (this.hitTimer > 0) {
this.hitTimer--;
this.gravity();
return;
}
if (this.attackTimer > 0) this.attackTimer--;
let dx = 0;
if (keys'a' || keys'arrowleft') { dx = -5; this.facing = -1; }
if (keys'd' || keys'arrowright') { dx = 5; this.facing = 1; }
this.x = Math.max(0, Math.min(W - this.w, this.x + dx));
if (jumpRequested && this.onGround) {
this.vy = -14;
this.onGround = false;
}
jumpRequested = false;
this.gravity();
}
attack() {
if (this.attackTimer <= 0 && this.hitTimer <= 0) this.attackTimer = 12;
}
}
// ================= 敌人 =================
class Enemy extends Fighter {
constructor(x, level = 1) {
super(x, GROUND - 60, 36, 60, '#c84a4a', 40 + level * 20);
this.speed = 1.6 + level * 0.4;
this.cd = 0;
}
update(player) {
if (this.attackTimer > 0) this.attackTimer--;
if (this.cd > 0) this.cd--;
if (this.hitTimer > 0) {
this.hitTimer--;
this.gravity();
return;
}
const dist = (player.x + player.w / 2) - (this.x + this.w / 2);
this.facing = dist > 0 ? 1 : -1;
if (Math.abs(dist) > 55) this.x += this.facing * this.speed;
if (Math.abs(dist) < 75 && this.cd <= 0) {
this.attackTimer = 15;
this.cd = 70;
}
this.gravity();
}
}
// ================= 状态 =================
let player, enemies, effects, state;
function reset() {
player = new Player();
enemies = [
new Enemy(320, 1),
new Enemy(560, 1),
new Enemy(780, 2)
];
effects = \[\];
state = 'playing';
jumpRequested = false;
}
// ================= 更新 =================
function update() {
if (state !== 'playing') return;
player.update();
// 玩家攻击判定
const pb = player.attackBox();
if (pb) {
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemiesi;
if (e.hitTimer > 0) continue;
if (hit(pb, e.box)) {
e.hp -= 12;
e.hitTimer = 8;
e.attackTimer = 0;
effects.push({ x: e.x + e.w / 2, y: e.y + e.h / 2, t: 10 });
if (e.hp <= 0) enemies.splice(i, 1);
}
}
}
// 敌人更新 + 攻击判定
for (const e of enemies) {
e.update(player);
const eb = e.attackBox();
if (eb && player.hitTimer <= 0 && !pb && hit(eb, player.box)) {
player.hp -= 8;
player.hitTimer = 15;
player.attackTimer = 0;
player.x += -e.facing * 25;
player.x = Math.max(0, Math.min(W - player.w, player.x));
}
}
// 打击特效衰减
for (let i = effects.length - 1; i >= 0; i--) {
if (--effectsi.t <= 0) effects.splice(i, 1);
}
// 胜负
if (player.hp <= 0) state = 'lose';
else if (enemies.length === 0) state = 'win';
}
// ================= 渲染 =================
function render() {
// 背景
ctx.fillStyle = '#1a1a24';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#2c2c3c';
ctx.lineWidth = 1;
for (let i = 0; i < 8; i++) {
ctx.strokeRect(i * 140 + 20, GROUND - 170, 90, 170);
}
// 地面
ctx.fillStyle = '#3a3330';
ctx.fillRect(0, GROUND, W, H - GROUND);
ctx.strokeStyle = '#6a5a4a';
ctx.beginPath();
ctx.moveTo(0, GROUND);
ctx.lineTo(W, GROUND);
ctx.stroke();
// 实体
for (const e of enemies) e.draw();
player.draw();
// 打击特效
for (const fx of effects) {
const k = fx.t / 10;
ctx.strokeStyle = `rgba(255,213,79,${k})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(fx.x, fx.y, 8 + (1 - k) * 18, 0, Math.PI * 2);
ctx.stroke();
}
// HUD
ctx.fillStyle = '#fff';
ctx.font = '20px monospace';
ctx.fillText(`HP ${Math.max(0, player.hp)}/100`, 20, 30);
ctx.fillText(`敌人 ${enemies.length}`, 20, 56);
ctx.fillStyle = '#888';
ctx.font = '16px monospace';
ctx.fillText('A/D 移动 Space/W 跳跃 J/Z 攻击', 20, H - 20);
// 结算
if (state !== 'playing') {
ctx.fillStyle = 'rgba(0,0,0,0.72)';
ctx.fillRect(0, 0, W, H);
ctx.textAlign = 'center';
ctx.font = 'bold 48px monospace';
ctx.fillStyle = state === 'win' ? '#ffd54f' : '#e84a4a';
ctx.fillText(state === 'win' ? 'DUNGEON CLEAR' : 'YOU DIED', W / 2, H / 2 - 20);
ctx.fillStyle = '#ccc';
ctx.font = '20px monospace';
ctx.fillText('按 R 重新开始', W / 2, H / 2 + 40);
ctx.textAlign = 'start';
}
}
// ================= 主循环 =================
function loop() {
update();
render();
requestAnimationFrame(loop);
}
reset();
loop();
})();
</script>
</body>
</html>
按键 功能
A / D 左右移动
Space / W 跳跃
J / Z 攻击
R 死亡或通关后重开