2048 Shooter

2048 Shooter

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
    <title>2048 Shooter</title>
    <style>
        * { user-select: none; -webkit-tap-highlight-color: transparent; box-sizing: border-box; }
        :root {
            --bg-deep: #050a12;
            --panel-bg: rgba(8, 14, 22, 0.95);
            --border-sharp: #2e4a6e;
            --glow-accent: #facc15;
        }
        body {
            margin: 0; height: 100vh; width: 100vw; overflow: hidden;
            background: radial-gradient(circle at 30% 10%, #0b1220, #010308);
            font-family: system-ui, -apple-system, sans-serif;
            display: flex; flex-direction: column; align-items: center; justify-content: center;
            color: white;
        }
        .game-container { width: 95%; max-width: 450px; height: 98vh; display: flex; flex-direction: column; gap: 8px; position: relative; justify-content: center; }

        .stats-panel { display: flex; justify-content: space-between; gap: 6px; flex-shrink: 0; }
        .stat-card { flex: 1; background: var(--panel-bg); border: 1px solid var(--border-sharp); padding: 8px 2px; text-align: center; border-radius: 4px; }
        .stat-label { font-size: 10px; color: #89a9dd; letter-spacing: 1px; text-transform: uppercase; }
        .stat-digit { font-size: 20px; font-weight: 800; color: #facc15; }

        .items-bar { display: flex; justify-content: space-between; gap: 6px; flex-shrink: 0; }
        .item-btn { 
            flex: 1; background: #16253b; border: 1px solid #3b82f6; color: #fff; 
            padding: 10px 2px; cursor: pointer; font-size: 12px; font-weight: bold; 
            display: flex; flex-direction: column; align-items: center; gap: 4px; border-radius: 4px; 
        }
        .item-btn.active { background: #facc15; color: #000; border-color: #fff; }
        .item-btn:disabled { opacity: 0.2; filter: grayscale(1); }
        .count-tag { font-size: 9px; background: rgba(0,0,0,0.4); padding: 1px 6px; border-radius: 10px; color: #facc15; }

        .canvas-wrapper { position: relative; flex-grow: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; background: rgba(0,0,0,0.2); border-radius: 8px; }
        canvas { display: block; background: #050a12; border: 2px solid #2e4a6e; touch-action: none; max-height: 100%; max-width: 100%; }

        #gameOverMask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.92); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 100; backdrop-filter: blur(10px); }
        .restart-btn { background: #facc15; color: #000; border: none; padding: 12px 40px; font-size: 18px; font-weight: bold; margin-top: 25px; border-radius: 4px; cursor: pointer; }

        .hint { font-size: 12px; color: #64748b; text-align: center; flex-shrink: 0; padding: 5px 0; min-height: 22px; }
    </style>
</head>
<body>

<div class="game-container">
    <div class="stats-panel">
        <div class="stat-card"><div class="stat-label">Score</div><div class="stat-digit" id="score">0</div></div>
        <div class="stat-card"><div class="stat-label">Next</div><div class="stat-digit" id="next" style="color:#2dd4bf">2</div></div>
        <div class="stat-card"><div class="stat-label">Best</div><div class="stat-digit" id="best">0</div></div>
    </div>

    <div class="items-bar">
        <button class="item-btn" id="btnHammer">🔨 碎裂 <span class="count-tag" id="countHammer">3</span></button>
        <button class="item-btn" id="btnSwap">⇄ 交換 <span class="count-tag" id="countSwap">3</span></button>
        <button class="item-btn" id="btnUndo">↩ 撤銷 <span class="count-tag" id="countUndo">3</span></button>
    </div>

    <div class="canvas-wrapper">
        <canvas id="gameCanvas"></canvas>
        <div id="gameOverMask">
            <div style="color:#facc15; font-size:36px; font-weight:900;">GAME OVER</div>
            <div style="margin-top:10px; color:white;">最終得分: <span id="finalScoreDisplay" style="color:#facc15; font-size:28px; font-weight:bold;">0</span></div>
            <button class="restart-btn" onclick="location.reload()">再試一次</button>
        </div>
    </div>

    <div class="hint" id="modeHint">● 滑動瞄準,抬手發射</div>
</div>

<script>
(function(){
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const ROWS = 9, COLS = 5;

    let cellSize, tiles = [], bullet = null, isFlying = false, isProcessing = false;
    let score = 0, best = localStorage.getItem('2048_best') || 0;
    let nextVal = 2, activeTool = null, swapTarget = null;
    let previewX = -100;

    let historyStack = null; 
    let itemCounts = { hammer: 3, swap: 3, undo: 3 };

    const colors = { 
        2: "#3B82F6", 4: "#2D9CDB", 8: "#2DC2BD", 16: "#20B2AA", 
        32: "#F97316", 64: "#F25E3A", 128: "#FFA63F", 256: "#FFB347", 
        512: "#FFC362", 1024: "#E55C4B", 2048: "#D93947" 
    };

    class Block {
        constructor(x, y, val) {
            this.x = x; this.y = y; this.targetY = y; this.val = val;
        }
        update() {
            if (Math.abs(this.y - this.targetY) > 1) this.y += (this.targetY - this.y) * 0.3;
            else this.y = this.targetY;
        }
        draw(isGhost = false) {
            const s = cellSize - 6;
            ctx.save();
            ctx.globalAlpha = isGhost ? 0.6 : 1.0;
            ctx.fillStyle = colors[this.val] || "#475569";
            if(ctx.roundRect) {
                ctx.beginPath(); ctx.roundRect(this.x - s/2, this.y - s/2, s, s, 6); ctx.fill();
            } else {
                ctx.fillRect(this.x - s/2, this.y - s/2, s, s);
            }
            ctx.strokeStyle = isGhost ? "#fff" : "rgba(255,255,255,0.3)";
            ctx.lineWidth = isGhost ? 2 : 1;
            ctx.strokeRect(this.x - s/2, this.y - s/2, s, s);
            ctx.fillStyle = "white";
            ctx.font = `bold ${cellSize * 0.35}px sans-serif`;
            ctx.textAlign = "center"; ctx.textBaseline = "middle";
            ctx.fillText(this.val, this.x, this.y);
            ctx.restore();
        }
    }

    const initLayout = () => {
        const wrapper = document.querySelector('.canvas-wrapper');
        cellSize = Math.min((wrapper.clientWidth-10)/COLS, (wrapper.clientHeight-10)/ROWS);
        canvas.width = cellSize * COLS;
        canvas.height = cellSize * ROWS;
    };

    const updateUI = () => {
        document.getElementById('score').innerText = score;
        document.getElementById('best').innerText = best;
        document.getElementById('next').innerText = nextVal;
        for (let key in itemCounts) {
            document.getElementById('count' + key.charAt(0).toUpperCase() + key.slice(1)).innerText = itemCounts[key];
            document.getElementById('btn' + key.charAt(0).toUpperCase() + key.slice(1)).disabled = (itemCounts[key] <= 0);
        }
    };

    const executePhysics = async () => {
        if (isProcessing) return;
        isProcessing = true;
        let changed = true;
        while (changed) {
            changed = false;
            // 重力
            for (let c = 0; c < COLS; c++) {
                let colX = c * cellSize + cellSize/2;
                let colBlocks = tiles.filter(t => Math.abs(t.x - colX) < 5).sort((a,b) => a.y - b.y);
                colBlocks.forEach((t, i) => {
                    let ty = i * cellSize + cellSize/2;
                    if (Math.abs(t.targetY - ty) > 1) { t.targetY = ty; changed = true; }
                });
            }
            if (changed) await new Promise(r => setTimeout(r, 80));

            // 合併
            for (let i = 0; i < tiles.length; i++) {
                for (let j = 0; j < tiles.length; j++) {
                    if (i === j) continue;
                    const a = tiles[i], b = tiles[j];
                    const isV = Math.abs(a.x - b.x) < 5 && Math.abs(a.targetY - (b.targetY - cellSize)) < 5;
                    const isH = Math.abs(a.targetY - b.targetY) < 5 && Math.abs(Math.abs(a.x - b.x) - cellSize) < 5;
                    if ((isV || isH) && a.val === b.val) {
                        if (Math.random() > 0.5) { b.val *= 2; score += b.val; tiles.splice(i, 1); }
                        else { a.val *= 2; score += a.val; tiles.splice(j, 1); }
                        changed = true; break;
                    }
                }
                if (changed) break;
            }
        }
        isProcessing = false; 
        updateUI();

        if (score > best) { best = score; localStorage.setItem('2048_best', best); }

        // 【修復關鍵】:檢查是否結束,並正確顯示當前最新分數
        if (tiles.some(t => t.targetY > canvas.height - cellSize * 1.2)) {
            document.getElementById('finalScoreDisplay').innerText = score; // 直接注入最新變數
            document.getElementById('gameOverMask').style.display = 'flex';
        }
    };

    const getPos = (e) => {
        const rect = canvas.getBoundingClientRect();
        const touch = e.touches?.[0] || e.changedTouches?.[0] || e;
        const x = (touch.clientX - rect.left) * (canvas.width / rect.width);
        const y = (touch.clientY - rect.top) * (canvas.height / rect.height);
        return { x, y, colX: Math.floor(x / cellSize) * cellSize + cellSize/2 };
    };

    const handleInput = (e) => {
        if (isProcessing || isFlying || document.getElementById('gameOverMask').style.display === 'flex') return;
        const { x, y, colX } = getPos(e);

        if (activeTool) {
            const target = tiles.find(t => Math.abs(t.x - colX) < cellSize/2 && Math.abs(t.y - y) < cellSize/2);
            if (target) {
                if (activeTool === 'hammer') {
                    saveHistory(); itemCounts.hammer--; tiles = tiles.filter(t => t !== target); executePhysics();
                } else if (activeTool === 'swap') {
                    if (!swapTarget) { swapTarget = target; return; }
                    if (target === swapTarget) { swapTarget = null; } 
                    else { saveHistory(); itemCounts.swap--; const v = swapTarget.val; swapTarget.val = target.val; target.val = v; executePhysics(); }
                }
            }
            activeTool = null; swapTarget = null;
            document.querySelectorAll('.item-btn').forEach(b => b.classList.remove('active'));
            return;
        }

        saveHistory();
        bullet = new Block(colX, canvas.height - cellSize/2, nextVal);
        isFlying = true;
        const maxV = tiles.length > 0 ? Math.max(...tiles.map(t => t.val)) : 8;
        const pool = [2, 2, 2, 4, 4, 8].filter(v => v <= Math.max(8, maxV/4));
        nextVal = pool[Math.floor(Math.random() * pool.length)];
        updateUI();
    };

    const saveHistory = () => {
        historyStack = { tiles: tiles.map(t => ({x: t.x, y: t.targetY, val: t.val})), score, nextVal };
    };

    initLayout();
    canvas.addEventListener('touchstart', (e) => { e.preventDefault(); previewX = getPos(e).colX; }, {passive:false});
    canvas.addEventListener('touchmove', (e) => { e.preventDefault(); previewX = getPos(e).colX; }, {passive:false});
    canvas.addEventListener('touchend', (e) => { e.preventDefault(); handleInput(e); });
    canvas.addEventListener('mousedown', (e) => { previewX = getPos(e).colX; });
    canvas.addEventListener('mousemove', (e) => { if(e.buttons > 0) previewX = getPos(e).colX; });
    canvas.addEventListener('mouseup', handleInput);

    document.getElementById('btnHammer').onclick = () => { activeTool = 'hammer'; document.getElementById('btnHammer').classList.add('active'); };
    document.getElementById('btnSwap').onclick = () => { activeTool = 'swap'; document.getElementById('btnSwap').classList.add('active'); };
    document.getElementById('btnUndo').onclick = () => {
        if (itemCounts.undo > 0 && historyStack) {
            itemCounts.undo--; score = historyStack.score; nextVal = historyStack.nextVal;
            tiles = historyStack.tiles.map(d => new Block(d.x, d.y, d.val));
            historyStack = null; updateUI();
        }
    };

    function animate() {
        ctx.clearRect(0,0,canvas.width,canvas.height);
        if (!isFlying && !isProcessing && !activeTool && previewX > 0) {
            ctx.fillStyle = "rgba(255,255,255,0.05)"; ctx.fillRect(previewX-cellSize/2, 0, cellSize, canvas.height);
            new Block(previewX, canvas.height-cellSize/2, nextVal).draw(true);
        }
        if (isFlying && bullet) {
            bullet.y -= (canvas.height / 20);
            if (bullet.y <= cellSize/2 || tiles.some(t => Math.abs(t.x-bullet.x)<5 && Math.abs(t.y-bullet.y)<cellSize*0.8)) {
                tiles.push(bullet); isFlying = false; bullet = null; executePhysics();
            }
        }
        tiles.forEach(t => { t.update(); t.draw(); });
        if (bullet) bullet.draw();
        if (swapTarget) { ctx.strokeStyle="#facc15"; ctx.lineWidth=3; ctx.strokeRect(swapTarget.x-cellSize/2, swapTarget.y-cellSize/2, cellSize, cellSize); }
        requestAnimationFrame(animate);
    }
    updateUI(); animate();
})();
</script>
</body>
</html>
上一篇