传奇纪元 · 高清像素 | 森林与洞穴
by UltraOtter17803 lines36.3 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>传奇纪元 · 高清像素 | 森林与洞穴</title>
<style>
* {
user-select: none;
-webkit-tap-highlight-color: transparent;
image-rendering: crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
body {
margin: 0;
min-height: 100vh;
background: #0a0c12;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Courier New', 'VT323', monospace;
overflow: hidden;
}
.game-container {
background: #000000cc;
border-radius: 0px;
padding: 8px;
box-shadow: 0 0 0 4px #7a5a3a, 0 0 0 8px #2c2418;
}
canvas {
display: block;
margin: 0 auto;
image-rendering: crisp-edges;
image-rendering: pixelated;
cursor: crosshair;
box-shadow: inset 0 0 0 2px #ebc47f;
}
.ui-panel {
margin-top: 14px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
background: #0d0f16f0;
padding: 6px 18px;
border: 2px solid #a77b2e;
font-family: 'Courier New', monospace;
font-weight: bold;
text-shadow: 2px 2px 0 #1e1a0c;
}
.stats {
background: #020108cc;
padding: 4px 12px;
border: 1px solid #e4b363;
color: #ffe6aa;
display: flex;
gap: 20px;
letter-spacing: 1px;
}
.stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 18px;
}
.skills {
display: flex;
gap: 8px;
background: #0b0e1a;
padding: 4px 12px;
border: 1px solid #bc8f4b;
}
.skill-btn {
background: #2c2a3c;
border: none;
color: #ffeaac;
font-family: 'Courier New', monospace;
font-weight: bold;
padding: 5px 12px;
cursor: pointer;
font-size: 14px;
box-shadow: inset 0 1px 0 #6f6a7e, 0 2px 0 #121016;
transition: 0.05s linear;
}
.skill-btn:active {
transform: translateY(1px);
box-shadow: inset 0 1px 0 #3f3a4e;
}
.job-select {
display: flex;
gap: 10px;
background: #14181fcc;
padding: 4px 14px;
border: 1px solid #b87c32;
}
.job-btn {
background: #322818;
border: none;
color: #ffdd99;
font-weight: bold;
padding: 4px 16px;
cursor: pointer;
font-family: monospace;
font-size: 15px;
transition: all 0.05s;
}
.job-btn.active {
background: #cb7b2a;
color: #201c0a;
box-shadow: inset 0 1px 2px #ffd58c;
}
.tip {
font-size: 12px;
background: #030407bb;
padding: 4px 12px;
border-left: 3px solid #f5bc70;
color: #cdc28c;
}
</style>
</head>
<body>
<div>
<div class="game-container">
<canvas id="gameCanvas" width="1000" height="600"></canvas>
<div class="ui-panel">
<div class="stats">
<div class="stat"><span>❤️</span><span id="hpValue">0</span>/<span id="maxHp">0</span></div>
<div class="stat"><span>💙</span><span id="mpValue">0</span>/<span id="maxMp">0</span></div>
<div class="stat"><span>⚔️</span><span id="atkValue">0</span></div>
<div class="stat"><span>⭐Lv</span><span id="level">1</span></div>
<div class="stat"><span>📊EXP</span><span id="expCur">0</span>/<span id="expNext">100</span></div>
</div>
<div class="skills">
<button class="skill-btn" id="normalAtk">🖱️ 攻击</button>
<button class="skill-btn" id="skillQ">[Q] 烈</button>
<button class="skill-btn" id="skillE">[E] 魂</button>
</div>
<div class="job-select">
<button class="job-btn" data-job="warrior">⚔️战士</button>
<button class="job-btn" data-job="mage">🔥法师</button>
<button class="job-btn" data-job="archer">🏹弓手</button>
</div>
<div class="tip">WASD移动 | 鼠标指向攻击/技能 | 传送门穿梭两界</div>
</div>
</div>
</div>
<script>
(function(){
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// 世界尺寸
const WORLD_WIDTH = 2800;
const WORLD_HEIGHT = 2000;
let camera = { x: 0, y: 0 };
// 职业系统
const JOBS = { WARRIOR: 'warrior', MAGE: 'mage', ARCHER: 'archer' };
let currentJob = JOBS.WARRIOR;
let player = {
x: 1400, y: 1000, radius: 14,
hp: 280, maxHp: 280, mp: 120, maxMp: 120,
baseAtk: 28, level: 1, exp: 0, expToNext: 100,
invincibleTimer: 0,
lastAttackTime: 0, lastQTime: 0, lastETime: 0,
attackCd: 580, qCd: 1950, eCd: 2400,
moveSpeed: 3.8
};
const jobStats = {
warrior: { hpBonus: 80, mpBonus: 0, atkBonus: 12, speed: 3.8, color: "#d88c4a", name:"战士" },
mage: { hpBonus: 30, mpBonus: 80, atkBonus: 18, speed: 3.5, color: "#5d9bff", name:"法师" },
archer: { hpBonus: 45, mpBonus: 40, atkBonus: 15, speed: 4.0, color: "#6ba96e", name:"弓手" }
};
function applyJobStats() {
let stats = jobStats[currentJob];
player.maxHp = 200 + stats.hpBonus + (player.level-1)*25;
player.maxMp = 80 + stats.mpBonus + (player.level-1)*15;
player.baseAtk = 22 + stats.atkBonus + Math.floor(player.level * 1.8);
if(player.hp > player.maxHp) player.hp = player.maxHp;
if(player.mp > player.maxMp) player.mp = player.maxMp;
player.moveSpeed = stats.speed;
updateUI();
}
function updateUI() {
document.getElementById('hpValue').innerText = Math.floor(player.hp);
document.getElementById('maxHp').innerText = player.maxHp;
document.getElementById('mpValue').innerText = Math.floor(player.mp);
document.getElementById('maxMp').innerText = player.maxMp;
document.getElementById('atkValue').innerText = player.baseAtk;
document.getElementById('level').innerText = player.level;
document.getElementById('expCur').innerText = player.exp;
document.getElementById('expNext').innerText = player.expToNext;
}
// 特效池
let floatingTexts = [];
let projectiles = [];
let aoes = [];
function addFloatingText(text, x, y, color="#ffaa77") {
floatingTexts.push({ text, x, y, life: 1.0, color });
}
function gainExp(amount) {
player.exp += amount;
while(player.exp >= player.expToNext) {
player.exp -= player.expToNext;
player.level++;
player.expToNext = Math.floor(80 + player.level * 25);
player.maxHp += 28;
player.maxMp += 12;
player.hp = player.maxHp;
player.mp = player.maxMp;
applyJobStats();
addFloatingText(`⭐ LV.${player.level} ⭐`, player.x, player.y-35, "#ffdd77");
}
updateUI();
}
// ----- 怪物体系(带警戒范围)-----
let monsters = [];
const MONSTER_TYPES = {
goblin: { hp: 68, atk: 18, def: 5, speed: 1.65, exp: 45, name:"哥布林" },
skeleton: { hp: 92, atk: 25, def: 7, speed: 1.85, exp: 72, name:"骷髅" },
orc: { hp: 148, atk: 33, def: 12, speed: 1.45, exp: 115, name:"兽人" }
};
class Monster {
constructor(x,y, type) {
this.x = x; this.y = y; this.radius = 16;
this.type = type;
let stats = MONSTER_TYPES[type];
this.hp = stats.hp;
this.maxHp = stats.hp;
this.atk = stats.atk;
this.def = stats.def;
this.speed = stats.speed;
this.expGive = stats.exp;
this.lastAttack = 0;
this.attackCd = 950;
this.aggroRange = 200;
this.state = "idle";
}
}
function spawnMonsters() {
monsters = [];
let count = 8 + Math.floor(Math.random() * 5);
for(let i=0;i<count;i++) {
let r = Math.random();
let type = r < 0.5 ? "goblin" : (r < 0.8 ? "skeleton" : "orc");
let x = 200 + Math.random() * (WORLD_WIDTH-400);
let y = 150 + Math.random() * (WORLD_HEIGHT-300);
if(Math.hypot(x-player.x, y-player.y) < 200) { x += 150; y += 100; }
monsters.push(new Monster(x,y,type));
}
}
function killMonster(idx) {
let m = monsters[idx];
if(!m) return;
gainExp(m.expGive);
monsters.splice(idx,1);
}
// ----- 固定建筑(树木/石柱)带碰撞-----
let staticProps = [];
function initStaticProps() {
staticProps = [];
// 森林树木
for(let i=0;i<55;i++) {
let x = 120 + Math.random() * 1200;
let y = 80 + Math.random() * 1800;
if(Math.hypot(x-1400, y-1000) < 100) continue;
staticProps.push({ type: "tree", x: x, y: y, w: 24, h: 28, solid: true });
}
// 洞穴岩石
for(let i=0;i<45;i++) {
let x = 1600 + Math.random() * 1100;
let y = 100 + Math.random() * 1800;
staticProps.push({ type: "rock", x: x, y: y, w: 22, h: 22, solid: true });
}
const extraTrees = [
{ x: 520, y: 720 }, { x: 890, y: 1340 }, { x: 250, y: 1600 }, { x: 1100, y: 480 },
{ x: 1350, y: 1850 }, { x: 1780, y: 380 }, { x: 2100, y: 1250 }, { x: 2400, y: 620 },
{ x: 2550, y: 1550 }
];
for(let t of extraTrees) staticProps.push({ type: "tree", x: t.x, y: t.y, w: 26, h: 30, solid: true });
const extraRocks = [
{ x: 1900, y: 900 }, { x: 2250, y: 1750 }, { x: 2650, y: 550 }, { x: 1700, y: 1420 }
];
for(let r of extraRocks) staticProps.push({ type: "rock", x: r.x, y: r.y, w: 24, h: 24, solid: true });
}
function collideWithProps(newX, newY, rad) {
let x = newX, y = newY;
if(x - rad < 0) x = rad;
if(x + rad > WORLD_WIDTH) x = WORLD_WIDTH - rad;
if(y - rad < 0) y = rad;
if(y + rad > WORLD_HEIGHT) y = WORLD_HEIGHT - rad;
for(let prop of staticProps) {
if(!prop.solid) continue;
let left = prop.x, right = prop.x + prop.w;
let top = prop.y, bottom = prop.y + prop.h;
let closestX = Math.max(left, Math.min(x, right));
let closestY = Math.max(top, Math.min(y, bottom));
let dx = x - closestX, dy = y - closestY;
let dist = Math.hypot(dx, dy);
if(dist < rad) {
let angle = Math.atan2(dy, dx);
let push = rad - dist;
x += Math.cos(angle) * push;
y += Math.sin(angle) * push;
}
}
return { x, y };
}
// 传送门系统
let currentMapId = 0; // 0:森林 1:洞穴
const teleportZones = [
{ x1: 100, y1: 60, x2: 200, y2: 160, toMap: 1, spawnPos: { x: 2620, y: 1840 }, label: "→洞穴" },
{ x1: 2580, y1: 1840, x2: 2700, y2: 1960, toMap: 0, spawnPos: { x: 150, y: 110 }, label: "→森林" }
];
function checkTeleport() {
for(let zone of teleportZones) {
if(player.x > zone.x1 && player.x < zone.x2 && player.y > zone.y1 && player.y < zone.y2) {
if(currentMapId === zone.toMap) continue;
currentMapId = zone.toMap;
player.x = zone.spawnPos.x;
player.y = zone.spawnPos.y;
spawnMonsters();
addFloatingText(currentMapId === 0 ? "🌲 翡翠森林 🌲" : "🪨 幽暗洞穴 🪨", player.x, player.y-40, "#aaffdd");
break;
}
}
}
// ----- 技能与攻击逻辑 (保持原样)-----
function normalAttack(worldX, worldY) {
let now = Date.now();
if(now - player.lastAttackTime < player.attackCd) return;
player.lastAttackTime = now;
let dmg = Math.floor(player.baseAtk + Math.random() * 12);
if(currentJob === JOBS.WARRIOR) {
let angle = Math.atan2(worldY - player.y, worldX - player.x);
let hit = false;
for(let i=0;i<monsters.length;i++) {
let m = monsters[i];
let dx = m.x - player.x, dy = m.y - player.y;
let dist = Math.hypot(dx,dy);
let ang = Math.atan2(dy,dx);
if(dist < 58 && Math.abs(ang-angle) < 1.2) {
let dealt = Math.max(6, dmg - m.def/2);
m.hp -= dealt;
addFloatingText(`-${dealt}`, m.x, m.y-12, "#ffaa77");
if(m.hp <= 0) killMonster(i--);
hit = true;
}
}
if(hit) aoes.push({ x:player.x, y:player.y, radius:48, duration:120, damage:0, endTime:now+120, color:"#ffb347" });
else addFloatingText("MISS", player.x+15, player.y-18, "#aaa");
}
else if(currentJob === JOBS.MAGE) {
let angle = Math.atan2(worldY - player.y, worldX - player.x);
let vx = Math.cos(angle)*10, vy = Math.sin(angle)*10;
projectiles.push({ x:player.x, y:player.y, vx, vy, radius:8, damage:dmg+7, owner:"player", color:"#e0aaff" });
}
else if(currentJob === JOBS.ARCHER) {
let angle = Math.atan2(worldY - player.y, worldX - player.x);
let vx = Math.cos(angle)*12, vy = Math.sin(angle)*12;
projectiles.push({ x:player.x, y:player.y, vx, vy, radius:6, damage:dmg+4, owner:"player", color:"#c6f7a0" });
}
}
function castSkillQ(mouseWX, mouseWY) {
let now = Date.now();
if(now - player.lastQTime < player.qCd) return;
player.lastQTime = now;
if(currentJob === JOBS.WARRIOR) {
aoes.push({ x:player.x, y:player.y, radius:75, duration:400, damage:Math.floor(player.baseAtk*1.65), endTime:now+400, color:"#ff8844" });
addFloatingText("🌀 旋风斩", player.x, player.y-25, "#ffaa66");
} else if(currentJob === JOBS.MAGE) {
let angle = Math.atan2(mouseWY - player.y, mouseWX - player.x);
let vx = Math.cos(angle)*8, vy = Math.sin(angle)*8;
projectiles.push({ x:player.x, y:player.y, vx, vy, radius:14, damage:Math.floor(player.baseAtk*2.1), owner:"player", color:"#ff6633", explode:true });
addFloatingText("🔥 大火球", player.x, player.y-20, "#ff9966");
} else if(currentJob === JOBS.ARCHER) {
let base = Math.atan2(mouseWY - player.y, mouseWX - player.x);
for(let i=-1;i<=1;i++) {
let ang = base + i*0.3;
let vx = Math.cos(ang)*11, vy = Math.sin(ang)*11;
projectiles.push({ x:player.x, y:player.y, vx, vy, radius:6, damage:Math.floor(player.baseAtk*0.95), owner:"player", color:"#e2ffb0" });
}
addFloatingText("🏹 散射", player.x, player.y-22, "#c6ff8a");
}
}
function castSkillE(mouseWX, mouseWY) {
let now = Date.now();
if(now - player.lastETime < player.eCd) return;
player.lastETime = now;
if(currentJob === JOBS.WARRIOR) {
let angle = Math.atan2(mouseWY - player.y, mouseWX - player.x);
let dist = 170, ex = player.x + Math.cos(angle)*dist, ey = player.y + Math.sin(angle)*dist;
let dmgVal = Math.floor(player.baseAtk * 1.85);
for(let i=0;i<monsters.length;i++) {
let m = monsters[i];
let t = ((m.x - player.x)*Math.cos(angle) + (m.y - player.y)*Math.sin(angle)) / dist;
if(t>0 && t<1.3) {
let perp = Math.abs((m.x - player.x)*Math.sin(angle) - (m.y - player.y)*Math.cos(angle));
if(perp < 48) {
let dealt = Math.max(8, dmgVal - m.def/2);
m.hp -= dealt;
addFloatingText(`-${dealt}`, m.x, m.y-14, "#ffaa66");
if(m.hp <=0) killMonster(i--);
}
}
}
aoes.push({ type:"line", sx:player.x, sy:player.y, ex, ey, duration:180, endTime:now+180 });
addFloatingText("⚡ 裂地斩", player.x, player.y-30, "#ffaa55");
} else if(currentJob === JOBS.MAGE) {
aoes.push({ x:player.x, y:player.y, radius:100, duration:450, damage:Math.floor(player.baseAtk*1.5), endTime:now+450, color:"#8ccbff" });
addFloatingText("❄️ 霜冻新星", player.x, player.y-28, "#b3ecff");
} else if(currentJob === JOBS.ARCHER) {
aoes.push({ x:mouseWX, y:mouseWY, radius:90, duration:750, damage:Math.floor(player.baseAtk*1.3), endTime:now+750, color:"#b3ff99" });
addFloatingText("🌧️ 箭雨", mouseWX, mouseWY-25, "#b9ff8c");
}
}
function updateProjectiles() {
for(let i=0;i<projectiles.length;i++) {
let p = projectiles[i];
p.x += p.vx; p.y += p.vy;
if(p.x < -200 || p.x > WORLD_WIDTH+200 || p.y < -200 || p.y > WORLD_HEIGHT+200) {
projectiles.splice(i,1); i--; continue;
}
let hit = false;
for(let j=0;j<monsters.length;j++) {
let m = monsters[j];
if(Math.hypot(p.x-m.x, p.y-m.y) < p.radius + m.radius) {
let dealt = Math.max(4, p.damage - m.def/2);
m.hp -= dealt;
addFloatingText(`-${dealt}`, m.x, m.y-10, "#ffaa66");
if(m.hp <= 0) killMonster(j--);
hit = true;
if(p.explode) aoes.push({ x:p.x, y:p.y, radius:52, duration:220, damage:p.damage*0.7, endTime:Date.now()+220, color:"#ff7744" });
break;
}
}
if(hit) { projectiles.splice(i,1); i--; }
}
}
function updateAoEs() {
let now = Date.now();
for(let i=0;i<aoes.length;i++) {
let a = aoes[i];
if(now > a.endTime) { aoes.splice(i,1); i--; continue; }
if(a.damage && a.damage>0) {
for(let j=0;j<monsters.length;j++) {
let m = monsters[j];
if(Math.hypot(m.x - a.x, m.y - a.y) < a.radius) {
let dealt = Math.max(5, a.damage - m.def/2);
m.hp -= dealt;
addFloatingText(`-${dealt}`, m.x, m.y-12, "#ffbb77");
if(m.hp <=0) killMonster(j--);
a.damage = 0;
}
}
}
}
}
function updateMonsters() {
let now = Date.now();
for(let i=0;i<monsters.length;i++) {
let m = monsters[i];
let dx = player.x - m.x, dy = player.y - m.y;
let distToPlayer = Math.hypot(dx,dy);
if(distToPlayer < m.aggroRange) m.state = "chase";
else if(distToPlayer > m.aggroRange + 50) m.state = "idle";
if(m.state === "chase" && distToPlayer > 2) {
let move = Math.min(m.speed, distToPlayer-18);
let normX = dx/distToPlayer, normY = dy/distToPlayer;
let newX = m.x + normX * move;
let newY = m.y + normY * move;
for(let prop of staticProps) {
if(!prop.solid) continue;
let left=prop.x, right=prop.x+prop.w, top=prop.y, bottom=prop.y+prop.h;
let closeX = Math.max(left, Math.min(newX, right));
let closeY = Math.max(top, Math.min(newY, bottom));
let ddx = newX - closeX, ddy = newY - closeY;
if(Math.hypot(ddx,ddy) < m.radius) {
let ang = Math.atan2(ddy,ddx);
let push = m.radius - Math.hypot(ddx,ddy);
newX += Math.cos(ang)*push;
newY += Math.sin(ang)*push;
}
}
m.x = Math.min(WORLD_WIDTH - m.radius, Math.max(m.radius, newX));
m.y = Math.min(WORLD_HEIGHT - m.radius, Math.max(m.radius, newY));
}
if(distToPlayer < 34 && now - m.lastAttack > m.attackCd && m.state === "chase") {
m.lastAttack = now;
if(player.invincibleTimer <= now) {
let dmg = Math.max(6, m.atk - Math.floor(player.level/3));
player.hp -= dmg;
addFloatingText(`💔-${dmg}`, player.x, player.y-20, "#ff7777");
player.invincibleTimer = now + 520;
if(player.hp <= 0) {
player.hp = player.maxHp;
player.mp = player.maxMp;
player.x = 1400; player.y = 1000;
addFloatingText("💀 勇者复生 💀", player.x, player.y-45, "#ff8888");
spawnMonsters();
}
updateUI();
}
}
}
}
const keys = { w:false, s:false, a:false, d:false };
function movePlayer() {
let mx=0, my=0;
if(keys.w) my--; if(keys.s) my++;
if(keys.a) mx--; if(keys.d) mx++;
if(mx!==0 || my!==0) {
let len = Math.hypot(mx,my);
mx /= len; my /= len;
let newX = player.x + mx * player.moveSpeed;
let newY = player.y + my * player.moveSpeed;
let corrected = collideWithProps(newX, newY, player.radius);
player.x = corrected.x; player.y = corrected.y;
}
}
function updateCamera() {
camera.x = player.x - canvas.width/2;
camera.y = player.y - canvas.height/2;
camera.x = Math.min(Math.max(camera.x, 0), WORLD_WIDTH - canvas.width);
camera.y = Math.min(Math.max(camera.y, 0), WORLD_HEIGHT - canvas.height);
}
// ========== 静态背景缓存(彻底解决闪烁)==========
let bgCanvas = null;
function buildStaticBackground() {
bgCanvas = document.createElement('canvas');
bgCanvas.width = WORLD_WIDTH;
bgCanvas.height = WORLD_HEIGHT;
let bgCtx = bgCanvas.getContext('2d');
bgCtx.imageSmoothingEnabled = false;
// 森林区域整体绘制 (0~1400 森林视觉,1400~2800其实也是森林逻辑,但为了区分,实际地图混合)
// 但我们按地图风格统一绘制,传送门切换时重绘背景缓存
if(currentMapId === 0) { // 森林
bgCtx.fillStyle = "#3f7845";
bgCtx.fillRect(0,0,WORLD_WIDTH,WORLD_HEIGHT);
// 草地细节静态噪点
for(let i=0;i<1800;i++) {
bgCtx.fillStyle = `#${Math.floor(0x448833 + Math.random() * 0x226622).toString(16)}`;
bgCtx.fillRect(Math.floor(Math.random()*WORLD_WIDTH), Math.floor(Math.random()*WORLD_HEIGHT), 2,2);
}
// 小花
bgCtx.fillStyle = "#ffc285";
for(let i=0;i<600;i++) bgCtx.fillRect(Math.floor(Math.random()*WORLD_WIDTH), Math.floor(Math.random()*WORLD_HEIGHT), 2,2);
} else { // 洞穴
bgCtx.fillStyle = "#3e2e2a";
bgCtx.fillRect(0,0,WORLD_WIDTH,WORLD_HEIGHT);
for(let i=0;i<2000;i++) {
bgCtx.fillStyle = `#${Math.floor(0x5f4c3c + Math.random() * 0x332211).toString(16)}`;
bgCtx.fillRect(Math.floor(Math.random()*WORLD_WIDTH), Math.floor(Math.random()*WORLD_HEIGHT), 2,2);
}
bgCtx.fillStyle = "#ffaa6644";
for(let i=0;i<400;i++) bgCtx.fillRect(Math.floor(Math.random()*WORLD_WIDTH), Math.floor(Math.random()*WORLD_HEIGHT), 2,2);
}
}
// 绘制静态建筑 (树木/岩石)
function drawPropsOnBg() {
for(let prop of staticProps) {
if(prop.type === "tree") {
ctx.fillStyle = "#6b4c3b";
ctx.fillRect(prop.x+6, prop.y+10, 10, 18);
ctx.fillStyle = "#2f7a2f";
ctx.fillRect(prop.x, prop.y, prop.w, 20);
ctx.fillStyle = "#3f9a2e";
ctx.fillRect(prop.x-4, prop.y+6, prop.w+8, 12);
} else if(prop.type === "rock") {
ctx.fillStyle = "#6f5e4a";
ctx.fillRect(prop.x, prop.y, prop.w, prop.h);
ctx.fillStyle = "#8b765a";
ctx.fillRect(prop.x+3, prop.y+3, prop.w-6, prop.h-6);
}
}
}
// 精致怪物绘制 (更高像素细节)
function drawMonster(m) {
let x=m.x, y=m.y;
if(m.type === "goblin") {
ctx.fillStyle = "#6f9e4a"; ctx.fillRect(x-10,y-12,20,16);
ctx.fillStyle = "#4e6b2f"; ctx.fillRect(x-12,y-12,4,10); ctx.fillRect(x+8,y-12,4,10);
ctx.fillStyle = "#222"; ctx.fillRect(x-5,y-8,4,4); ctx.fillRect(x+1,y-8,4,4);
ctx.fillStyle = "#aa5533"; ctx.fillRect(x-3,y-2,6,3);
ctx.fillStyle = "#ddaa66"; ctx.fillRect(x-2,y-6,2,2); ctx.fillRect(x+1,y-6,2,2);
} else if(m.type === "skeleton") {
ctx.fillStyle = "#bcbcbc"; ctx.fillRect(x-9,y-14,18,18);
ctx.fillStyle = "#6b6b6b"; ctx.fillRect(x-12,y-8,4,10); ctx.fillRect(x+8,y-8,4,10);
ctx.fillStyle = "#000"; ctx.fillRect(x-6,y-9,3,4); ctx.fillRect(x+3,y-9,3,4);
ctx.fillStyle = "#993333"; ctx.fillRect(x-2,y-3,4,2);
ctx.fillStyle = "#ffaaaa"; ctx.fillRect(x-1,y-7,2,2); ctx.fillRect(x+1,y-7,2,2);
} else { // 兽人
ctx.fillStyle = "#9b6a38"; ctx.fillRect(x-11,y-13,22,18);
ctx.fillStyle = "#7a4e2a"; ctx.fillRect(x-14,y-10,6,12); ctx.fillRect(x+8,y-10,6,12);
ctx.fillStyle = "#442200"; ctx.fillRect(x-6,y-9,4,5); ctx.fillRect(x+2,y-9,4,5);
ctx.fillStyle = "#ddaa66"; ctx.fillRect(x-4,y-3,8,4);
ctx.fillStyle = "#eeddbb"; ctx.fillRect(x-2,y-1,2,4); ctx.fillRect(x+1,y-1,2,4);
ctx.fillStyle = "#cc8855"; ctx.fillRect(x-1,y-8,2,3); ctx.fillRect(x+1,y-8,2,3);
}
// 血条
ctx.fillStyle = "#aa2222"; ctx.fillRect(x-14, y-18, 28, 4);
ctx.fillStyle = "#33dd33"; ctx.fillRect(x-14, y-18, 28 * (m.hp/m.maxHp), 4);
}
// 精致玩家绘制
function drawPlayer() {
let x=player.x, y=player.y;
// 身体
ctx.fillStyle = jobStats[currentJob].color; ctx.fillRect(x-8,y-12,16,20);
// 头部
ctx.fillStyle = "#fdd7a8"; ctx.fillRect(x-7,y-18,14,10);
// 眼睛高光
ctx.fillStyle = "#ffffff"; ctx.fillRect(x-5,y-15,3,3); ctx.fillRect(x+2,y-15,3,3);
ctx.fillStyle = "#000000"; ctx.fillRect(x-4,y-15,2,2); ctx.fillRect(x+3,y-15,2,2);
if(currentJob === "warrior") {
ctx.fillStyle = "#a55c2c"; ctx.fillRect(x-9,y-20,18,6);
ctx.fillStyle = "#c0c0c0"; ctx.fillRect(x-3,y-22,6,4);
ctx.fillStyle = "#aaaaee"; ctx.fillRect(x+9,y-8,4,18); ctx.fillStyle="#ddbb77"; ctx.fillRect(x+12,y-4,3,5);
ctx.fillStyle = "#ffffff"; ctx.fillRect(x+10,y-2,2,2);
} else if(currentJob === "mage") {
ctx.fillStyle = "#aaccff"; ctx.fillRect(x-8,y-22,16,6);
ctx.fillStyle = "#aa8866"; ctx.fillRect(x+9,y-12,3,20); ctx.fillStyle="#ff66cc"; ctx.fillRect(x+10,y-16,5,6);
ctx.fillStyle = "#ffff99"; ctx.fillRect(x+11,y-10,2,4);
} else { // 弓手
ctx.fillStyle = "#8b9a5e"; ctx.fillRect(x-9,y-21,18,5);
ctx.fillStyle = "#c9ae74"; ctx.fillRect(x+8,y-10,4,16); ctx.fillStyle="#ffdd99"; ctx.fillRect(x+10,y-12,6,4);
ctx.fillStyle = "#ddcc88"; ctx.fillRect(x+11,y-8,2,6);
}
ctx.fillStyle = "#ffffaa"; ctx.font = "bold 14px monospace";
ctx.fillText(`${jobStats[currentJob].name} Lv${player.level}`, x-20, y-27);
}
function drawDynamic() {
ctx.save();
ctx.translate(-camera.x, -camera.y);
// 绘制静态背景缓存 (无闪烁)
if(bgCanvas) ctx.drawImage(bgCanvas, 0, 0);
drawPropsOnBg();
// 传送门特效
for(let zone of teleportZones) {
ctx.fillStyle = "#a36effaa"; ctx.fillRect(zone.x1, zone.y1, zone.x2-zone.x1, zone.y2-zone.y1);
ctx.fillStyle = "#f3e5c0"; ctx.font = "bold 20px monospace";
ctx.fillText(zone.label, zone.x1+12, zone.y1+32);
}
// 怪物
for(let m of monsters) drawMonster(m);
// 投射物和特效
for(let p of projectiles) { ctx.fillStyle=p.color; ctx.fillRect(p.x-4,p.y-4,8,8); }
for(let a of aoes) {
if(a.radius) { ctx.globalAlpha=0.55; ctx.fillStyle=a.color||"#ffaa66"; ctx.beginPath(); ctx.arc(a.x,a.y,a.radius,0,Math.PI*2); ctx.fill(); ctx.globalAlpha=1; }
if(a.type==="line") { ctx.beginPath(); ctx.moveTo(a.sx,a.sy); ctx.lineTo(a.ex,a.ey); ctx.lineWidth=8; ctx.strokeStyle="#efb56e"; ctx.stroke(); }
}
for(let ft of floatingTexts) {
ctx.fillStyle=ft.color; ctx.font="bold 18px monospace";
ctx.fillText(ft.text, ft.x-15, ft.y-(1-ft.life)*20);
ft.life-=0.02;
}
floatingTexts = floatingTexts.filter(ft => ft.life>0);
drawPlayer();
ctx.restore();
}
// 地图切换时重建背景
function rebuildBackgroundOnMapChange() {
buildStaticBackground();
}
// 重写checkTeleport加入背景重建
const originalCheckTeleport = checkTeleport;
window.checkTeleport = function() {
let oldMap = currentMapId;
originalCheckTeleport();
if(oldMap !== currentMapId) {
rebuildBackgroundOnMapChange();
}
};
// 覆盖
function patchedCheckTeleport() {
let oldMap = currentMapId;
for(let zone of teleportZones) {
if(player.x > zone.x1 && player.x < zone.x2 && player.y > zone.y1 && player.y < zone.y2) {
if(currentMapId === zone.toMap) continue;
currentMapId = zone.toMap;
player.x = zone.spawnPos.x;
player.y = zone.spawnPos.y;
spawnMonsters();
addFloatingText(currentMapId === 0 ? "🌲 翡翠森林 🌲" : "🪨 幽暗洞穴 🪨", player.x, player.y-40, "#aaffdd");
rebuildBackgroundOnMapChange();
break;
}
}
}
// 游戏主循环
function gameLoop() {
movePlayer();
patchedCheckTeleport();
updateMonsters();
updateProjectiles();
updateAoEs();
updateCamera();
drawDynamic();
requestAnimationFrame(gameLoop);
}
function initEvents() {
window.addEventListener('keydown', (e) => {
let k = e.key.toLowerCase();
if(k === 'w') keys.w=true; if(k === 's') keys.s=true;
if(k === 'a') keys.a=true; if(k === 'd') keys.d=true;
if(k === 'q') { e.preventDefault(); castSkillQ(player.x+30, player.y); }
if(k === 'e') { e.preventDefault(); castSkillE(player.x+40, player.y); }
});
window.addEventListener('keyup', (e) => {
let k = e.key.toLowerCase();
if(k === 'w') keys.w=false; if(k === 's') keys.s=false;
if(k === 'a') keys.a=false; if(k === 'd') keys.d=false;
});
canvas.addEventListener('mousedown', (e) => {
e.preventDefault();
let rect = canvas.getBoundingClientRect();
let sx = canvas.width/rect.width, sy = canvas.height/rect.height;
let mx = (e.clientX - rect.left)*sx, my = (e.clientY - rect.top)*sy;
let worldX = mx + camera.x, worldY = my + camera.y;
if(e.button === 0) normalAttack(worldX, worldY);
});
document.querySelectorAll('.job-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.job-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentJob = btn.dataset.job;
applyJobStats();
player.hp = player.maxHp; player.mp = player.maxMp;
updateUI();
});
});
document.getElementById('skillQ').onclick = () => castSkillQ(player.x+35, player.y);
document.getElementById('skillE').onclick = () => castSkillE(player.x+35, player.y);
document.getElementById('normalAtk').onclick = () => normalAttack(player.x+45, player.y);
}
function init() {
initStaticProps();
applyJobStats();
player.hp = player.maxHp; player.mp = player.maxMp;
player.x = 1400; player.y = 1000;
spawnMonsters();
buildStaticBackground(); // 预先生成静态背景
updateUI();
initEvents();
gameLoop();
}
init();
})();
</script>
</body>
</html>Game Source: 传奇纪元 · 高清像素 | 森林与洞穴
Creator: UltraOtter17
Libraries: none
Complexity: complex (803 lines, 36.3 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: -ultraotter17" to link back to the original. Then publish at arcadelab.ai/publish.