🎮ArcadeLab

传奇纪元 · 高清像素 | 森林与洞穴

by UltraOtter17
803 lines36.3 KB
▶ Play
<!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.