一些和AI相关的讨论
回复
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5840
注册时间: 2022年 11月 18日 17:36
联系:

用國內(能直連的)AI寫紙飛機遊戲

帖子 ejsoon »

改進:
一,排行榜應有序號1.2.3……。

二,當網頁打開時,將自動生成一個用戶名(排行榜最大一個guest加1)。這個名稱將不能更改。

三,遊戲結束時將顯示當前的排名情況,並自動保存至排行榜,同一個guest將只收錄最高分。

四,如果超越了相同用戶名的最高分,則顯示「創造歷史最高分」,如果沒有,則顯示「歷史最高分為…」。

代码: 全选

改進:
一,排行榜應有序號1.2.3……。

二,當網頁打開時,將自動生成一個用戶名(排行榜最大一個guest加1)。這個名稱將不能更改。

三,遊戲結束時將顯示當前的排名情況,並自動保存至排行榜,同一個guest將只收錄最高分。

四,如果超越了相同用戶名的最高分,則顯示「創造歷史最高分」,如果沒有,則顯示「歷史最高分為…」。
附件
紙飛機2.0.html.7z
(5.19 KiB) 已下载 1 次
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5840
注册时间: 2022年 11月 18日 17:36
联系:

Re: 用國內(能直連的)AI寫紙飛機遊戲

帖子 ejsoon »

gemini 3.1 flash:

這是一個非常好的優化建議。為了實現這些需求,我們需要調整排行榜的資料結構(從「名稱」改為「主鍵名稱」)、調整 UI 下方的顯示邏輯,並在 JS 中修改保存邏輯。

以下是修改後的程式碼核心部分(請替換掉原本的 JavaScript 內容):
修改後的 JavaScript 部分

代码: 全选

<script>
    // ... 前面的設定 (CONFIG, DOM 元素等保持不變)

    // 排行榜機制
    let leaderboards = JSON.parse(localStorage.getItem('paperPlaneLB')) || [];
    let guestCounter = parseInt(localStorage.getItem('paperPlaneGuestCnt')) || 1;
    
    // 二、自動生成一個不可更改的用戶名
    const myUsername = `Guest${guestCounter}`;
    localStorage.setItem('paperPlaneGuestCnt', guestCounter + 1); // 預佔下一個號碼

    // 更新排行榜 UI
    function updateLeaderboardUI() {
        let html = '';
        // 一、加入序號
        leaderboards.forEach((entry, index) => {
            html += `<tr><td>${index + 1}</td><td>${entry.name}</td><td>${entry.score}</td><td>${entry.time}s</td></tr>`;
        });
        if(html === '') html = '<tr><td colspan="4">暫無記錄</td></tr>';
        document.getElementById('menuLBBody').innerHTML = html;
        document.getElementById('gameOverLBBody').innerHTML = html;
    }

    /** 遊戲結束 */
    function gameOver() {
        state.isPlaying = false;
        plane.isDead = true;
        planeLeft.setAttribute('fill', '#FF5252');
        planeRight.setAttribute('fill', '#D32F2F');

        document.getElementById('finalScore').innerText = state.score;
        
        // 三、檢查該用戶名是否已存在,並進行比較
        const existingEntry = leaderboards.find(l => l.name === myUsername);
        const recordMsg = document.getElementById('recordMsg');
        
        // 四、超越或維持紀錄的邏輯提示
        if (!existingEntry || state.score > existingEntry.score) {
            recordMsg.innerHTML = "🎉 創造歷史最高分!";
            recordMsg.style.color = "#4CAF50";
        } else {
            recordMsg.innerHTML = `歷史最高分為: ${existingEntry.score}`;
            recordMsg.style.color = "#333";
        }

        setTimeout(() => {
            uiGameOver.classList.remove('hidden');
        }, 800);
    }

    // 更新保存按鈕事件
    document.getElementById('saveRestartBtn').addEventListener('click', () => {
        // 三、處理保存邏輯:只收錄同一個用戶的最高分
        const existingIndex = leaderboards.findIndex(l => l.name === myUsername);
        const newData = {
            name: myUsername,
            score: state.score,
            time: state.time,
            date: getFormattedDate()
        };

        if (existingIndex !== -1) {
            if (state.score > leaderboards[existingIndex].score) {
                leaderboards[existingIndex] = newData;
            }
        } else {
            leaderboards.push(newData);
        }

        leaderboards.sort((a, b) => b.score - a.score);
        leaderboards = leaderboards.slice(0, 10);
        localStorage.setItem('paperPlaneLB', JSON.stringify(leaderboards));

        startGame();
    });

    // ... (遊戲主循環保持不變)
</script>
UI 部分需要微調

請在 HTML 的 gameOverUI 區塊中,將原本的 <input> 移除,並加入一個顯示訊息的區塊:

代码: 全选

<!-- 遊戲結束 UI 內的調整 -->
<div class="ui-layer hidden" id="gameOverUI">
    <h2 style="color: #FF1744;">遊戲結束!</h2>
    <p style="font-size: 20px;">玩家: <b id="playerNameDisplay"></b></p> <!-- 新增顯示名字 -->
    <p style="font-size: 20px; font-weight: bold;">最終分數: <span id="finalScore" style="color:#FF1744; font-size:28px;">0</span></p>
    <p id="recordMsg" style="font-weight:bold; margin-bottom:10px;"></p> <!-- 四、新增提示訊息 -->
    
    <button id="saveRestartBtn">保存並重新開始</button>
    <!-- ... 排行榜表格 -->
</div>
給你的建議:

名字顯示:在 startGame 函式中,加入 document.getElementById('playerNameDisplay').innerText = myUsername; 確保遊戲結束時能看到你的名字。
CSS 調整:因為移除了 <input>,現在的遊戲結算畫面對比更清晰,你可以直接讓用戶在開始前知道自己的編號(例如在 HUD 或 Menu 顯示 您是:GuestX 的字樣)。
資料持久化:使用 localStorage 是最簡單的實作,這對單機瀏覽器遊戲非常合適。透過 JSON.stringify 可以方便儲存複雜的陣列結構。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5840
注册时间: 2022年 11月 18日 17:36
联系:

Re: 用國內(能直連的)AI寫紙飛機遊戲

帖子 ejsoon »

改進:
去掉排行榜,當前會話的最高分數及其用時將記錄在一個var變量中,當頁面刷新則重新開始計。

如果超越了最高分,則顯示「創造歷史最高分:…」,如果沒有,則顯示「歷史最高分為…」。

增加函式,使外部(例如通過iframe)能夠探知遊戲是否結束以及結束後的分數和用時。


代码: 全选

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>紙飛機極速下落</title>
    <style>
        /* 基礎與卡通風格設定 */
        :root {
            --bg-top: #E0F7FA; /* 淺藍色 */
            --bg-bottom: #C8E6C9; /* 淺綠色 */
            --primary: #4CAF50;
            --text-color: #333;
        }

        body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #333;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            overflow: hidden;
            touch-action: none; /* 防止手機端預設滑動行為 */
        }

        #game-wrapper {
            position: relative;
            width: 100%;
            max-width: 450px;
            height: 100vh;
            max-height: 800px;
            background: linear-gradient(to bottom, var(--bg-top), var(--bg-bottom));
            border: 4px solid #333;
            box-shadow: 0px 0px 20px rgba(0,0,0,0.5);
            overflow: hidden;
        }

        /* 畫布與 SVG 飛機 */
        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        #plane-svg {
            position: absolute;
            width: 50px;
            height: 50px;
            top: 0; left: 0;
            /* 設置中心定位,後續由 JS 控制 transform 來實現偽 3D 轉向 */
            transform-origin: center center;
            pointer-events: none;
            z-index: 10;
            transition: fill 0.2s;
        }

        /* UI 層 */
        .ui-layer {
            position: absolute;
            top: 0; left: 0;
            width: 100%; height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background: rgba(255, 255, 255, 0.85);
            z-index: 20;
            backdrop-filter: blur(3px);
        }

        .hidden { display: none !important; }

        .hud {
            position: absolute;
            top: 15px; left: 15px; right: 15px;
            display: flex;
            justify-content: space-between;
            font-size: 20px;
            font-weight: 900;
            color: #333;
            text-shadow: 2px 2px 0px #fff;
            z-index: 15;
            pointer-events: none;
        }

        /* 按鈕與輸入框 */
        button {
            padding: 12px 30px;
            font-size: 20px;
            font-weight: bold;
            color: #fff;
            background-color: var(--primary);
            border: 3px solid #333;
            border-radius: 12px;
            cursor: pointer;
            box-shadow: 4px 4px 0px #333;
            margin-bottom: 20px;
            transition: transform 0.1s, box-shadow 0.1s;
        }

        button:active {
            transform: translate(4px, 4px);
            box-shadow: 0px 0px 0px #333;
        }

        input[type="text"] {
            padding: 10px;
            font-size: 16px;
            font-weight: bold;
            border: 3px solid #333;
            border-radius: 8px;
            margin-bottom: 15px;
            text-align: center;
            width: 220px;
            outline: none;
        }

        /* 排行榜 */
        .leaderboard {
            width: 85%;
            max-height: 250px;
            overflow-y: auto;
            background: #fff;
            border: 3px solid #333;
            border-radius: 8px;
            padding: 10px;
            box-shadow: inset 0px 0px 10px rgba(0,0,0,0.1);
        }

        table { width: 100%; border-collapse: collapse; font-size: 14px; text-align: center; }
        th { background-color: #eee; padding: 8px 5px; border-bottom: 2px solid #333; }
        td { padding: 8px 5px; border-bottom: 1px dashed #ccc; font-weight: bold;}
        h2 { margin-top: 0; font-size: 32px; color: #333; text-shadow: 2px 2px 0px #fff; }
        
        /* 觸控提示區塊(僅測試視覺用,預設透明) */
        .touch-zone {
            position: absolute;
            top: 0; bottom: 0; width: 50%;
            z-index: 5;
        }
    </style>
</head>
<body>

<div id="game-wrapper">
    <!-- HUD 資訊 -->
    <div class="hud" id="hud">
        <div id="scoreDisplay">分數: 0</div>
        <div id="timeDisplay">時間: 0.0s</div>
    </div>

    <!-- 障礙物渲染畫布 -->
    <canvas id="gameCanvas"></canvas>

    <!-- 偽三維 SVG 紙飛機 (利用右半邊陰影加上 CSS Transform 展現 3D 感) -->
    <svg id="plane-svg" viewBox="-25 -25 50 50">
        <!-- 飛機主體 (左翼與機身) -->
        <polygon id="planeLeft" points="0,20 -20,-15 0,-5" fill="#FFFFFF" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
        <!-- 飛機右翼 (加深顏色以呈現陰影立體感) -->
        <polygon id="planeRight" points="0,20 0,-5 20,-15" fill="#E0E0E0" stroke="#333" stroke-width="2" stroke-linejoin="round"/>
    </svg>

    <!-- 主選單 UI -->
    <div class="ui-layer" id="menuUI">
        <h2>紙飛機極速下落</h2>
        <button id="startBtn">開始遊戲</button>
        <div class="leaderboard">
            <table>
                <thead><tr><th>玩家</th><th>分數</th><th>用時</th><th>日期</th></tr></thead>
                <tbody id="menuLBBody"></tbody>
            </table>
        </div>
    </div>

    <!-- 遊戲結束 UI -->
    <div class="ui-layer hidden" id="gameOverUI">
        <h2 style="color: #FF1744;">遊戲結束!</h2>
        <p style="font-size: 20px; font-weight: bold;">最終分數: <span id="finalScore" style="color:#FF1744; font-size:28px;">0</span></p>
        <input type="text" id="usernameInput" placeholder="輸入玩家名稱" maxlength="12">
        <button id="saveRestartBtn">保存並重新開始</button>
        <div class="leaderboard">
            <table>
                <thead><tr><th>玩家</th><th>分數</th><th>用時</th><th>日期</th></tr></thead>
                <tbody id="gameOverLBBody"></tbody>
            </table>
        </div>
    </div>
</div>

<script>
    /** 
     * 遊戲參數配置 (可在這裡調節難度)
     */
    const CONFIG = {
        blockDensity: 160,       // 矩形出現的縱向間距 (越小越密)
        blockMinW: 100,          // 方塊最小寬度
        blockMaxW: 240,          // 方塊最大寬度
        blockHeight: 20,         // 方塊高度 (較短)
        baseFallSpeed: 9,        // 垂直向下時的最快速度
        turnSpeed: 0.08,         // 轉向靈敏度
        maxAngle: Math.PI / 2.2  // 最大傾斜角度
    };

    // DOM 元素獲取
    const wrapper = document.getElementById('game-wrapper');
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const planeSvg = document.getElementById('plane-svg');
    const planeLeft = document.getElementById('planeLeft');
    const planeRight = document.getElementById('planeRight');
    
    const uiMenu = document.getElementById('menuUI');
    const uiGameOver = document.getElementById('gameOverUI');
    const scoreDisp = document.getElementById('scoreDisplay');
    const timeDisp = document.getElementById('timeDisplay');
    
    let canvasW, canvasH;
    let animationFrameId;

    // 遊戲狀態
    let state = {
        isPlaying: false,
        score: 0,
        time: 0,
        startTime: 0,
        cameraY: 0,
        lastSpawnY: 0
    };

    // 飛機物件
    let plane = {
        x: 0,
        worldY: 0,
        screenY: 0,
        angle: 0,
        isDead: false
    };

    let blocks = [];
    let keys = { left: false, right: false };

    // 排行榜機制
    let leaderboards = JSON.parse(localStorage.getItem('paperPlaneLB')) || [];
    let guestCounter = parseInt(localStorage.getItem('paperPlaneGuestCnt')) || 1;

    // 自適應畫布大小
    function resizeCanvas() {
        canvasW = wrapper.clientWidth;
        canvasH = wrapper.clientHeight;
        canvas.width = canvasW;
        canvas.height = canvasH;
    }
    window.addEventListener('resize', resizeCanvas);
    resizeCanvas();

    // 鍵盤控制
    window.addEventListener('keydown', (e) => {
        if(e.code === 'ArrowLeft') keys.left = true;
        if(e.code === 'ArrowRight') keys.right = true;
    });
    window.addEventListener('keyup', (e) => {
        if(e.code === 'ArrowLeft') keys.left = false;
        if(e.code === 'ArrowRight') keys.right = false;
    });

    // 手機端左右螢幕觸控控制
    wrapper.addEventListener('touchstart', (e) => {
        if (!state.isPlaying) return;
        for (let i = 0; i < e.changedTouches.length; i++) {
            let touch = e.changedTouches[i];
            // 判斷點擊位置是否在屏幕左半邊
            if (touch.clientX < window.innerWidth / 2) keys.left = true;
            else keys.right = true;
        }
    });
    wrapper.addEventListener('touchend', (e) => {
        for (let i = 0; i < e.changedTouches.length; i++) {
            let touch = e.changedTouches[i];
            if (touch.clientX < window.innerWidth / 2) keys.left = false;
            else keys.right = false;
        }
    });
    wrapper.addEventListener('touchcancel', () => { keys.left = false; keys.right = false; });

    // 時間格式化
    function getFormattedDate() {
        const d = new Date();
        const pad = n => n.toString().padStart(2, '0');
        return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
    }

    // 更新排行榜 UI
    function updateLeaderboardUI() {
        let html = '';
        leaderboards.forEach(entry => {
            html += `<tr><td>${entry.name}</td><td>${entry.score}</td><td>${entry.time}s</td><td>${entry.date}</td></tr>`;
        });
        if(html === '') html = '<tr><td colspan="4">暫無記錄</td></tr>';
        document.getElementById('menuLBBody').innerHTML = html;
        document.getElementById('gameOverLBBody').innerHTML = html;
    }

    /** 啟動遊戲 */
    function startGame() {
        uiMenu.classList.add('hidden');
        uiGameOver.classList.add('hidden');
        
        // 初始化飛機:最左最上方,以最傾斜向右的角度滑翔
        plane.x = 40;
        plane.worldY = 40;
        plane.screenY = 40;
        plane.angle = CONFIG.maxAngle; // 極限向右傾斜
        plane.isDead = false;

        // 恢復正常顏色
        planeLeft.setAttribute('fill', '#FFFFFF');
        planeRight.setAttribute('fill', '#E0E0E0');

        state.isPlaying = true;
        state.score = 0;
        state.time = 0;
        state.startTime = performance.now();
        state.cameraY = 0;
        state.lastSpawnY = canvasH;
        
        blocks = [];
        keys.left = false;
        keys.right = false;

        updateLeaderboardUI();
        requestAnimationFrame(gameLoop);
    }

    /** 遊戲結束 */
    function gameOver() {
        state.isPlaying = false;
        plane.isDead = true;

        // 飛機變紅色
        planeLeft.setAttribute('fill', '#FF5252');
        planeRight.setAttribute('fill', '#D32F2F');

        // 更新最終分數與預設名字
        document.getElementById('finalScore').innerText = state.score;
        document.getElementById('usernameInput').value = `Guest${guestCounter}`;
        
        setTimeout(() => {
            uiGameOver.classList.remove('hidden');
        }, 800); // 稍微延遲顯示 UI 讓玩家看到撞擊畫面
    }

    /** 遊戲主循環 */
    function gameLoop(timestamp) {
        if (!state.isPlaying) return;

        // 1. 更新時間與分數
        state.time = ((timestamp - state.startTime) / 1000).toFixed(1);
        state.score = Math.floor(plane.worldY / 100);
        timeDisp.innerText = `時間: ${state.time}s`;
        scoreDisp.innerText = `分數: ${state.score}`;

        // 2. 處理轉向 (角度控制)
        if (keys.left) plane.angle -= CONFIG.turnSpeed;
        if (keys.right) plane.angle += CONFIG.turnSpeed;
        
        // 限制最大傾斜角
        plane.angle = Math.max(-CONFIG.maxAngle, Math.min(CONFIG.maxAngle, plane.angle));

        // 3. 計算下落速度 (垂直向下角度為0時最快,角度越大越慢)
        let angleRatio = Math.abs(plane.angle) / CONFIG.maxAngle;
        let currentSpeed = CONFIG.baseFallSpeed * (1 - (angleRatio * 0.6)); 

        // 4. 更新飛機世界座標 (0度是筆直朝下)
        plane.worldY += currentSpeed * Math.cos(plane.angle);
        plane.x += currentSpeed * Math.sin(plane.angle);

        // 邊界限制 (保持在螢幕左右內)
        if (plane.x < 20) { plane.x = 20; plane.angle = 0; }
        if (plane.x > canvasW - 20) { plane.x = canvasW - 20; plane.angle = 0; }

        // 5. 攝像機跟隨 (到達畫面中線後)
        if (plane.worldY > canvasH / 2) {
            state.cameraY = plane.worldY - canvasH / 2;
            plane.screenY = canvasH / 2;
        } else {
            plane.screenY = plane.worldY;
        }

        // 6. 生成障礙物方塊
        while (state.lastSpawnY < plane.worldY + canvasH) {
            state.lastSpawnY += CONFIG.blockDensity;
            let w = Math.random() * (CONFIG.blockMaxW - CONFIG.blockMinW) + CONFIG.blockMinW;
            let x = Math.random() * (canvasW - w);
            blocks.push({
                x: x, 
                worldY: state.lastSpawnY, 
                w: w, 
                h: CONFIG.blockHeight, 
                isHit: false 
            });
        }

        // 清理越界的舊方塊以釋放記憶體
        blocks = blocks.filter(b => b.worldY > state.cameraY - 50);

        // 7. 更新 SVG 偽三維渲染 (利用 CSS transform 旋轉及 3D 翻轉)
        // translate 置中, rotate 控制平面轉向, rotateY 控制側翻展現 3D 效果
        let rotateDeg = -plane.angle * (180 / Math.PI); // SVG預設向上,但我們初始設計朝下,需要反轉
        let rollDeg = plane.angle * 45; // 側翻角度
        planeSvg.style.transform = `translate(calc(${plane.x}px - 50%), calc(${plane.screenY}px - 50%)) rotate(${rotateDeg}deg) rotateY(${rollDeg}deg)`;

        // 8. 繪製畫面與碰撞檢測
        ctx.clearRect(0, 0, canvasW, canvasH);
        
        let planeHitbox = { x: plane.x - 10, y: plane.screenY - 15, w: 20, h: 30 };

        blocks.forEach(b => {
            let screenY = b.worldY - state.cameraY;
            
            // 碰撞檢測 (AABB)
            if (
                planeHitbox.x < b.x + b.w &&
                planeHitbox.x + planeHitbox.w > b.x &&
                planeHitbox.y < screenY + b.h &&
                planeHitbox.y + planeHitbox.h > screenY
            ) {
                b.isHit = true;
                gameOver();
            }

            // 繪製粉紅矩形方塊
            ctx.fillStyle = b.isHit ? '#FF1744' : '#FF80AB';
            ctx.strokeStyle = '#333';
            ctx.lineWidth = 3;
            ctx.beginPath();
            ctx.roundRect(b.x, screenY, b.w, b.h, 6);
            ctx.fill();
            ctx.stroke();
        });

        if (state.isPlaying) {
            animationFrameId = requestAnimationFrame(gameLoop);
        }
    }

    // 按鈕事件綁定
    document.getElementById('startBtn').addEventListener('click', startGame);
    
    document.getElementById('saveRestartBtn').addEventListener('click', () => {
        let name = document.getElementById('usernameInput').value.trim();
        if(!name) name = `Guest${guestCounter}`;
        
        // 如果使用了預設 GuestN,則計數器加 1
        if(name === `Guest${guestCounter}`) {
            guestCounter++;
            localStorage.setItem('paperPlaneGuestCnt', guestCounter);
        }

        // 保存紀錄
        leaderboards.push({
            name: name,
            score: state.score,
            time: state.time,
            date: getFormattedDate()
        });

        // 排序並只保留前 10 名
        leaderboards.sort((a, b) => b.score - a.score);
        leaderboards = leaderboards.slice(0, 10);
        localStorage.setItem('paperPlaneLB', JSON.stringify(leaderboards));

        startGame();
    });

    // 初始渲染排行榜
    updateLeaderboardUI();
</script>

</body>
</html>


改進:
去掉排行榜,當前會話的最高分數及其用時將記錄在一個var變量中,當頁面刷新則重新開始計。

如果超越了最高分,則顯示「創造歷史最高分:…」,如果沒有,則顯示「歷史最高分為…」。

增加函式,使外部(例如通過iframe)能夠探知遊戲是否結束以及結束後的分數和用時。
https://ejsoon.vip/
弈趣極光:享受思維樂趣
头像
ejsoon
一枝独秀
一枝独秀
帖子: 5840
注册时间: 2022年 11月 18日 17:36
联系:

Re: 用國內(能直連的)AI寫紙飛機遊戲

帖子 ejsoon »

deepseek v4 pro:

代码: 全选

> OK let me now produce the final HTML. I'll be comprehensive and make sure everything works.

> Let me also fix a small issue: the original code uses `ctx.roundRect` but that's a method on CanvasRenderingContext2D that might need a polyfill in some browsers. Actually, `roundRect` is supported in Chrome 99+, Firefox 112+, Safari 15.4+. It should be fine for modern browsers. The original code used it so I'll keep it.

> Now I'll write the full HTML.

This is a complete HTML document for a "纸飞机极速下落" (Paper Plane Speed Drop) game. It's a side-scrolling avoidance game where you control a paper plane falling through obstacles, built with Canvas and SVG for a pseudo-3D effect.
```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>紙飛機極速下落</title>
    <style>
        :root {
            --bg-top: #E0F7FA;
            --bg-bottom: #C8E6C9;
            --primary: #4CAF50;
            --text-color: #333;
        }

        body {
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #333;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            overflow: hidden;
            touch-action: none;
        }

        #game-wrapper {
            position: relative;
            width: 100%;
            max-width: 450px;
            height: 100vh;
            max-height: 800px;
            background: linear-gradient(to bottom, var(--bg-top), var(--bg-bottom));
            border: 4px solid #333;
            box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.5);
            overflow: hidden;
        }

        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }

        #plane-svg {
            position: absolute;
            width: 50px;
            height: 50px;
            top: 0;
            left: 0;
            transform-origin: center center;
            pointer-events: none;
            z-index: 10;
            transition: fill 0.2s;
        }

        .ui-layer {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            background: rgba(255, 255, 255, 0.85);
            z-index: 20;
            backdrop-filter: blur(3px);
            padding: 20px;
            box-sizing: border-box;
        }

        .hidden {
            display: none !important;
        }

        .hud {
            position: absolute;
            top: 15px;
            left: 15px;
            right: 15px;
            display: flex;
            justify-content: space-between;
            font-size: 20px;
            font-weight: 900;
            color: #333;
            text-shadow: 2px 2px 0px #fff;
            z-index: 15;
            pointer-events: none;
        }

        button {
            padding: 12px 30px;
            font-size: 20px;
            font-weight: bold;
            color: #fff;
            background-color: var(--primary);
            border: 3px solid #333;
            border-radius: 12px;
            cursor: pointer;
            box-shadow: 4px 4px 0px #333;
            margin-bottom: 20px;
            transition: transform 0.1s, box-shadow 0.1s;
            -webkit-tap-highlight-color: transparent;
        }

        button:active {
            transform: translate(4px, 4px);
            box-shadow: 0px 0px 0px #333;
        }

        h2 {
            margin-top: 0;
            font-size: 32px;
            color: #333;
            text-shadow: 2px 2px 0px #fff;
            text-align: center;
        }

        .stat-text {
            font-size: 20px;
            font-weight: bold;
            color: #333;
            margin: 6px 0;
        }

        .highlight-score {
            color: #FF1744;
            font-size: 28px;
            font-weight: 900;
        }

        .highscore-msg {
            font-size: 20px;
            font-weight: bold;
            margin: 10px 0 20px 0;
            padding: 10px 20px;
            border-radius: 10px;
            background: #FFF8E1;
            border: 2px dashed #FF9800;
            color: #E65100;
            text-align: center;
            animation: popIn 0.4s ease-out;
        }

        .highscore-msg.new-record {
            background: #FFF3E0;
            border-color: #FF5722;
            color: #BF360C;
            animation: popIn 0.4s ease-out, pulse 0.8s ease-in-out infinite;
        }

        @keyframes popIn {
            0% {
                transform: scale(0.7);
                opacity: 0;
            }
            70% {
                transform: scale(1.05);
            }
            100% {
                transform: scale(1);
                opacity: 1;
            }
        }

        @keyframes pulse {
            0%,
            100% {
                box-shadow: 0 0 0 0 rgba(255, 87, 34, 0.4);
            }
            50% {
                box-shadow: 0 0 0 12px rgba(255, 87, 34, 0);
            }
        }

        .menu-highscore {
            font-size: 16px;
            font-weight: bold;
            color: #FF6D00;
            margin-top: 8px;
            text-align: center;
            min-height: 22px;
        }
    </style>
</head>
<body>

    <div id="game-wrapper">
        <!-- HUD -->
        <div class="hud" id="hud">
            <div id="scoreDisplay">分數: 0</div>
            <div id="timeDisplay">時間: 0.0s</div>
        </div>

        <!-- 遊戲畫布 -->
        <canvas id="gameCanvas"></canvas>

        <!-- SVG 紙飛機 -->
        <svg id="plane-svg" viewBox="-25 -25 50 50">
            <polygon id="planeLeft" points="0,20 -20,-15 0,-5" fill="#FFFFFF" stroke="#333" stroke-width="2" stroke-linejoin="round" />
            <polygon id="planeRight" points="0,20 0,-5 20,-15" fill="#E0E0E0" stroke="#333" stroke-width="2" stroke-linejoin="round" />
        </svg>

        <!-- 主選單 -->
        <div class="ui-layer" id="menuUI">
            <h2>紙飛機極速下落</h2>
            <button id="startBtn">開始遊戲</button>
            <div id="menuHighScore" class="menu-highscore"></div>
        </div>

        <!-- 遊戲結束 -->
        <div class="ui-layer hidden" id="gameOverUI">
            <h2 style="color: #FF1744;">遊戲結束!</h2>
            <p class="stat-text">最終分數: <span id="finalScore" class="highlight-score">0</span></p>
            <p class="stat-text">用時: <span id="finalTime" style="font-size:22px; font-weight:900;">0.0s</span></p>
            <div id="highScoreMsg"></div>
            <button id="restartBtn">重新開始</button>
        </div>
    </div>

    <script>
        /**
         * 遊戲參數配置
         */
        const CONFIG = {
            blockDensity: 160,
            blockMinW: 100,
            blockMaxW: 240,
            blockHeight: 20,
            baseFallSpeed: 9,
            turnSpeed: 0.08,
            maxAngle: Math.PI / 2.2
        };

        // ──────────────── DOM 元素 ────────────────
        const wrapper = document.getElementById('game-wrapper');
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const planeSvg = document.getElementById('plane-svg');
        const planeLeft = document.getElementById('planeLeft');
        const planeRight = document.getElementById('planeRight');

        const uiMenu = document.getElementById('menuUI');
        const uiGameOver = document.getElementById('gameOverUI');
        const scoreDisp = document.getElementById('scoreDisplay');
        const timeDisp = document.getElementById('timeDisplay');
        const menuHighScoreEl = document.getElementById('menuHighScore');

        let canvasW, canvasH;
        let animationFrameId;

        // ──────────────── 遊戲狀態 ────────────────
        let state = {
            isPlaying: false,
            score: 0,
            time: 0,
            startTime: 0,
            cameraY: 0,
            lastSpawnY: 0
        };

        let plane = {
            x: 0,
            worldY: 0,
            screenY: 0,
            angle: 0,
            isDead: false
        };

        let blocks = [];
        let keys = { left: false, right: false };

        // ──────────────── 本輪會話最高分記錄(頁面刷新即重置) ────────────────
        var sessionHighScore = -1; // -1 表示尚未有任何遊戲記錄
        var sessionHighScoreTime = 0;
        var lastGameScore = 0;
        var lastGameTime = 0;
        var isGameOverState = false;
        var lastGameIsNewHighScore = false;

        // ──────────────── 外部 API(供 iframe 父頁面調用) ────────────────
        window.PaperPlaneGame = {
            /**
             * 取得完整遊戲狀態
             * @returns {Object} 包含 isPlaying, isGameOver, lastScore, lastTime,
             *                   sessionHighScore, sessionHighScoreTime, isNewHighScore
             */
            getState: function() {
                return {
                    isPlaying: state.isPlaying,
                    isGameOver: isGameOverState,
                    lastScore: lastGameScore,
                    lastTime: lastGameTime,
                    sessionHighScore: sessionHighScore,
                    sessionHighScoreTime: sessionHighScoreTime,
                    isNewHighScore: lastGameIsNewHighScore
                };
            },

            /** 遊戲是否正在進行中 */
            isPlaying: function() { return state.isPlaying; },

            /** 遊戲是否已結束(遊戲結束畫面顯示中) */
            isGameOver: function() { return isGameOverState; },

            /** 取得上一次遊戲的分數 */
            getLastScore: function() { return lastGameScore; },

            /** 取得上一次遊戲的用時(秒) */
            getLastTime: function() { return lastGameTime; },

            /** 取得本輪會話最高分 */
            getSessionHighScore: function() { return sessionHighScore; },

            /** 取得本輪會話最高分的用時 */
            getSessionHighScoreTime: function() { return sessionHighScoreTime; },

            /** 上一次遊戲是否刷新了最高分 */
            isNewHighScore: function() { return lastGameIsNewHighScore; },

            /**
             * 遊戲結束時的回調(同一來源的父頁面可設置此屬性)
             * @type {Function|null} callback(score, time, isNewHighScore)
             * @example
             * window.PaperPlaneGame.onGameOver = function(score, time, isNew) {
             *     console.log('Game over! Score:', score);
             * };
             */
            onGameOver: null
        };

        /**
         * 向父窗口發送 postMessage(適用於跨來源 iframe 嵌入)
         */
        function notifyParentGameOver(score, time, isNewHighScore) {
            if (window.parent && window.parent !== window) {
                try {
                    window.parent.postMessage({
                        source: 'paperPlaneGame',
                        event: 'gameOver',
                        data: {
                            score: score,
                            time: time,
                            isNewHighScore: isNewHighScore,
                            sessionHighScore: sessionHighScore,
                            sessionHighScoreTime: sessionHighScoreTime
                        }
                    }, '*');
                } catch (e) {
                    // 忽略跨來源通訊錯誤
                }
            }
        }

        // ──────────────── 輔助函式 ────────────────
        function resizeCanvas() {
            canvasW = wrapper.clientWidth;
            canvasH = wrapper.clientHeight;
            canvas.width = canvasW;
            canvas.height = canvasH;
        }
        window.addEventListener('resize', resizeCanvas);
        resizeCanvas();

        /** 更新選單上的本輪最高分顯示 */
        function updateMenuHighScore() {
            if (sessionHighScore >= 0) {
                menuHighScoreEl.textContent =
                    '本次最高分:' + sessionHighScore + ' 分 (用時 ' + sessionHighScoreTime + 's)';
            } else {
                menuHighScoreEl.textContent = '';
            }
        }

        // ──────────────── 鍵盤控制 ────────────────
        window.addEventListener('keydown', (e) => {
            if (e.code === 'ArrowLeft') keys.left = true;
            if (e.code === 'ArrowRight') keys.right = true;
        });
        window.addEventListener('keyup', (e) => {
            if (e.code === 'ArrowLeft') keys.left = false;
            if (e.code === 'ArrowRight') keys.right = false;
        });

        // ──────────────── 觸控控制(手機端左右半屏) ────────────────
        wrapper.addEventListener('touchstart', (e) => {
            if (!state.isPlaying) return;
            for (let i = 0; i < e.changedTouches.length; i++) {
                let touch = e.changedTouches[i];
                if (touch.clientX < window.innerWidth / 2) keys.left = true;
                else keys.right = true;
            }
        });
        wrapper.addEventListener('touchend', (e) => {
            for (let i = 0; i < e.changedTouches.length; i++) {
                let touch = e.changedTouches[i];
                if (touch.clientX < window.innerWidth / 2) keys.left = false;
                else keys.right = false;
            }
        });
        wrapper.addEventListener('touchcancel', () => {
            keys.left = false;
            keys.right = false;
        });

        // ──────────────── 遊戲流程 ────────────────
        function startGame() {
            uiMenu.classList.add('hidden');
            uiGameOver.classList.add('hidden');
            isGameOverState = false;

            // 初始化飛機
            plane.x = 40;
            plane.worldY = 40;
            plane.screenY = 40;
            plane.angle = CONFIG.maxAngle;
            plane.isDead = false;

            // 恢復飛機顏色
            planeLeft.setAttribute('fill', '#FFFFFF');
            planeRight.setAttribute('fill', '#E0E0E0');

            state.isPlaying = true;
            state.score = 0;
            state.time = 0;
            state.startTime = performance.now();
            state.cameraY = 0;
            state.lastSpawnY = canvasH;

            blocks = [];
            keys.left = false;
            keys.right = false;

            updateMenuHighScore();
            requestAnimationFrame(gameLoop);
        }

        function gameOver() {
            state.isPlaying = false;
            plane.isDead = true;
            isGameOverState = true;

            // 飛機變紅色
            planeLeft.setAttribute('fill', '#FF5252');
            planeRight.setAttribute('fill', '#D32F2F');

            // 記錄本次成績
            lastGameScore = state.score;
            lastGameTime = state.time;

            // 判斷是否刷新本輪會話最高分
            let isNewHighScore = (lastGameScore > sessionHighScore);
            lastGameIsNewHighScore = isNewHighScore;

            if (isNewHighScore) {
                sessionHighScore = lastGameScore;
                sessionHighScoreTime = lastGameTime;
            }

            // 更新遊戲結束 UI
            document.getElementById('finalScore').innerText = lastGameScore;
            document.getElementById('finalTime').innerText = lastGameTime + 's';

            const highScoreMsgDiv = document.getElementById('highScoreMsg');
            if (isNewHighScore) {
                highScoreMsgDiv.className = 'highscore-msg new-record';
                highScoreMsgDiv.innerHTML = '🎉 創造歷史最高分:<strong>' + sessionHighScore +
                    '</strong> 分!<br><small>用時 ' + sessionHighScoreTime + 's</small>';
            } else {
                highScoreMsgDiv.className = 'highscore-msg';
                highScoreMsgDiv.innerHTML = '歷史最高分為 <strong>' + sessionHighScore +
                    '</strong> 分<br><small>(用時 ' + sessionHighScoreTime + 's)</small>';
            }

            // 呼叫外部回調(同來源 iframe)
            if (typeof window.PaperPlaneGame.onGameOver === 'function') {
                try {
                    window.PaperPlaneGame.onGameOver(lastGameScore, lastGameTime, isNewHighScore);
                } catch (e) {
                    // 忽略回調中的錯誤
                }
            }

            // 向父窗口發送 postMessage(跨來源 iframe)
            notifyParentGameOver(lastGameScore, lastGameTime, isNewHighScore);

            // 更新選單最高分
            updateMenuHighScore();

            // 延遲顯示結束畫面
            setTimeout(() => {
                uiGameOver.classList.remove('hidden');
            }, 800);
        }

        function gameLoop(timestamp) {
            if (!state.isPlaying) return;

            // 更新時間與分數
            state.time = ((timestamp - state.startTime) / 1000).toFixed(1);
            state.score = Math.floor(plane.worldY / 100);
            timeDisp.innerText = '時間: ' + state.time + 's';
            scoreDisp.innerText = '分數: ' + state.score;

            // 處理轉向
            if (keys.left) plane.angle -= CONFIG.turnSpeed;
            if (keys.right) plane.angle += CONFIG.turnSpeed;
            plane.angle = Math.max(-CONFIG.maxAngle, Math.min(CONFIG.maxAngle, plane.angle));

            // 計算下落速度
            let angleRatio = Math.abs(plane.angle) / CONFIG.maxAngle;
            let currentSpeed = CONFIG.baseFallSpeed * (1 - (angleRatio * 0.6));

            // 更新飛機世界座標
            plane.worldY += currentSpeed * Math.cos(plane.angle);
            plane.x += currentSpeed * Math.sin(plane.angle);

            // 邊界限制
            if (plane.x < 20) { plane.x = 20;
                plane.angle = 0; }
            if (plane.x > canvasW - 20) { plane.x = canvasW - 20;
                plane.angle = 0; }

            // 攝像機跟隨
            if (plane.worldY > canvasH / 2) {
                state.cameraY = plane.worldY - canvasH / 2;
                plane.screenY = canvasH / 2;
            } else {
                plane.screenY = plane.worldY;
            }

            // 生成障礙物
            while (state.lastSpawnY < plane.worldY + canvasH) {
                state.lastSpawnY += CONFIG.blockDensity;
                let w = Math.random() * (CONFIG.blockMaxW - CONFIG.blockMinW) + CONFIG.blockMinW;
                let x = Math.random() * (canvasW - w);
                blocks.push({
                    x: x,
                    worldY: state.lastSpawnY,
                    w: w,
                    h: CONFIG.blockHeight,
                    isHit: false
                });
            }

            // 清理舊方塊
            blocks = blocks.filter(b => b.worldY > state.cameraY - 50);

            // 更新 SVG 飛機偽三維渲染
            let rotateDeg = -plane.angle * (180 / Math.PI);
            let rollDeg = plane.angle * 45;
            planeSvg.style.transform =
                'translate(calc(' + plane.x + 'px - 50%), calc(' + plane.screenY + 'px - 50%)) rotate(' +
                rotateDeg + 'deg) rotateY(' + rollDeg + 'deg)';

            // 繪製畫面與碰撞檢測
            ctx.clearRect(0, 0, canvasW, canvasH);

            let planeHitbox = { x: plane.x - 10, y: plane.screenY - 15, w: 20, h: 30 };

            blocks.forEach(b => {
                let screenY = b.worldY - state.cameraY;

                // AABB 碰撞檢測
                if (
                    planeHitbox.x < b.x + b.w &&
                    planeHitbox.x + planeHitbox.w > b.x &&
                    planeHitbox.y < screenY + b.h &&
                    planeHitbox.y + planeHitbox.h > screenY
                ) {
                    b.isHit = true;
                    gameOver();
                }

                // 繪製方塊
                ctx.fillStyle = b.isHit ? '#FF1744' : '#FF80AB';
                ctx.strokeStyle = '#333';
                ctx.lineWidth = 3;
                ctx.beginPath();
                if (ctx.roundRect) {
                    ctx.roundRect(b.x, screenY, b.w, b.h, 6);
                } else {
                    // fallback for older browsers
                    ctx.rect(b.x, screenY, b.w, b.h);
                }
                ctx.fill();
                ctx.stroke();
            });

            if (state.isPlaying) {
                animationFrameId = requestAnimationFrame(gameLoop);
            }
        }

        // ──────────────── 按鈕事件 ────────────────
        document.getElementById('startBtn').addEventListener('click', startGame);

        document.getElementById('restartBtn').addEventListener('click', () => {
            startGame();
        });

        // ──────────────── 初始渲染 ────────────────
        updateMenuHighScore();
    </script>

</body>
</html>
https://ejsoon.vip/
弈趣極光:享受思維樂趣
回复
  • 相似主题
    回复总数
    阅读次数
    最新帖子

在线用户

正浏览此版面之用户: 没有注册用户 和 1 访客