DeepSeek深度训练的网页小游戏 — Black 8(高级版)

AI2周前发布 beixibaobao
9 0 0

____simple_html_dom__voku__html_wrapper____>

Black 8 (Advanced) — 继Black 8 Pro之后继续深度训练的作品

主要功能提升:

增强了球杆拉升动画和击球音效等,提升沉浸式体验。

增加了挑战模式,实现更多玩法。

DeepSeek深度训练的网页小游戏 -- Black 8(高级版)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <title>Black 8 · Back to Splash</title>
    <style>
        * {
            box-sizing: border-box;
            -webkit-tap-highlight-color: transparent;
            user-select: none;
        }
        html, body {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
        body {
            background: url('https://os.v.madlive.cn/idcmadlive/2026/03/lianchi6.jpg') no-repeat center center fixed;
            background-size: cover;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
            position: relative;
        }
        .loading-overlay {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(31, 34, 51, 0.95);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            z-index: 9999;
            transition: opacity 0.5s ease;
            font-size: 4vmin;
            color: #ffd966;
            backdrop-filter: blur(5px);
        }
        .spinner {
            width: 10vmin;
            height: 10vmin;
            border: 1vmin solid rgba(255,209,102,0.3);
            border-top-color: #ffd166;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-bottom: 2vmin;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
        .loading-text {
            font-size: 4vmin;
            margin-top: 2vmin;
        }
        /* splash screen (mode select) */
        .splash-screen {
            position: fixed;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(10, 20, 10, 0.9);
            backdrop-filter: blur(8px);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10000;
            transition: opacity 0.4s ease;
        }
        .splash-card {
            background: #2d4a2d;
            border: 4px solid #ffd966;
            border-radius: 8vmin;
            padding: 6vmin 8vmin;
            text-align: center;
            box-shadow: 0 20px 40px black;
            max-width: 600px;
            width: 85%;
        }
        .splash-title {
            color: #ffd966;
            font-size: 8vmin;
            font-weight: bold;
            margin-bottom: 5vmin;
            text-shadow: 3px 3px 0 #1f3a1f;
        }
        .mode-buttons {
            display: flex;
            gap: 4vmin;
            justify-content: center;
            flex-wrap: wrap;
        }
        .mode-btn {
            background: #3d5c3a;
            border: 3px solid #e3b87c;
            color: #ffefc0;
            font-size: 5vmin;
            font-weight: bold;
            padding: 3vmin 6vmin;
            border-radius: 6vmin;
            box-shadow: 0 5px 0 #1d2e1b;
            cursor: pointer;
            transition: 0.1s;
            min-width: 26vmin;
        }
        .mode-btn:active {
            transform: translateY(5px);
            box-shadow: 0 1px 0 #1d2e1b;
        }
        .mode-desc {
            color: #ddb87b;
            font-size: 3.5vmin;
            margin-top: 5vmin;
            border-top: 2px dashed #b88c4a;
            padding-top: 3vmin;
        }
        .game-container {
            background: rgba(61, 43, 26, 0.85);
            backdrop-filter: blur(5px);
            padding: 1.5vmin 2vmin 2vmin 2vmin;
            border-radius: 4vmin;
            box-shadow: 0 20px 30px rgba(0,0,0,0.6), inset 2px 2px 8px #b87c4b;
            border: 2px solid #aa6e3a;
            position: relative;
            width: 95%;
            max-width: 820px;
            margin: 0 auto;
        }
        .game-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 1vmin;
        }
        .header-logo {
            height: 7vmin;
            width: auto;
            border-radius: 1vmin;
            border: 0.2vmin solid rgba(255,209,102,0.4);
            background: rgba(0,0,0,0.2);
            cursor: pointer;
        }
        .game-title {
            text-align: center;
            color: #ffd966;
            font-size: 5vmin;
            font-weight: bold;
            text-shadow: 3px 3px 0 #4f3a1e;
            letter-spacing: 2px;
            flex: 1;
        }
        canvas {
            display: block;
            width: 100%;
            height: auto;
            border-radius: 2.5vmin;
            background: #1e3b2a;
            box-shadow: inset 0 0 0 2px #7b5a3c, 0 10px 15px rgba(0,0,0,0.5);
            touch-action: none;
            cursor: crosshair;
        }
        .status-bar {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 2vmin 1vmin 1vmin 1vmin;
            color: #f7e9c3;
            text-shadow: 2px 2px 0 #4f3a1e;
            font-weight: bold;
            font-size: 5vmin;
        }
        .stroke-box {
            background: #2f4d2e;
            padding: 1vmin 5vmin;
            border-radius: 10vmin;
            border: 2px solid #dbb062;
            box-shadow: inset 0 2px 5px #0f2b0e;
            letter-spacing: 2px;
        }
        .bottom-panel {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 1vmin 1vmin 0 1vmin;
            gap: 2vmin;
            flex-wrap: wrap;
        }
        .power-meter {
            background: #5b4330;
            padding: 1vmin 3vmin;
            border-radius: 8vmin;
            border: 2px solid #edc27a;
            display: flex;
            align-items: center;
            gap: 2vmin;
            color: #ffdd99;
            font-size: 4vmin;
            font-weight: bold;
            flex: 1;
            min-width: 200px;
        }
        .bar-bg {
            width: 100%;
            height: 4vmin;
            background: #2a1e12;
            border-radius: 2vmin;
            border: 1px solid #ac8b5b;
            overflow: hidden;
        }
        .bar-fill {
            width: 20%;
            height: 100%;
            background: linear-gradient(90deg, #f9b81b, #f55d3e);
            transition: width 0.03s;
        }
        .button-group {
            display: flex;
            align-items: center;
            gap: 1.5vmin;
            flex-shrink: 0;
        }
        .action-btn {
            background: #3d5c3a;
            border: 2px solid #e3b87c;
            color: #ffefc0;
            font-size: 3.5vmin;
            padding: 0.8vmin 2.5vmin;
            border-radius: 5vmin;
            font-weight: bold;
            box-shadow: 0 3px 0 #1d2e1b;
            cursor: pointer;
            white-space: nowrap;
            min-width: 10vmin;
            text-align: center;
            transition: all 0.1s ease;
        }
        .action-btn:active {
            transform: translateY(3px);
            box-shadow: 0 1px 0 #1d2e1b;
        }
        .action-btn.mute {
            background: #5b4330;
            border-color: #edc27a;
            min-width: 8vmin;
            padding: 0.8vmin 1.5vmin;
        }
        .action-btn.next {
            background: #4a7a4a;
            border-color: #ffd966;
        }
        .hint {
            color: #ffd966;
            font-size: 3.2vmin;
            padding: 1vmin;
            background: #2d4a2d;
            border-radius: 4vmin;
            margin: 1vmin 0;
            text-align: center;
            border: 1px solid #b88c4a;
        }
        .again-overlay {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.4);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 20;
            backdrop-filter: blur(3px);
            transition: opacity 0.3s;
        }
        .again-card {
            background: #3d5c3a;
            border: 4px solid #ffd966;
            border-radius: 8vmin;
            padding: 5vmin 10vmin;
            text-align: center;
            box-shadow: 0 10px 30px black;
        }
        .again-card .again-title {
            color: #ffd966;
            font-size: 7vmin;
            font-weight: bold;
            margin-bottom: 5vmin;
            text-shadow: 2px 2px 0 #1f3a1f;
        }
        .again-btn {
            background: #f7b731;
            border: none;
            color: #1e3b1e;
            font-size: 6vmin;
            font-weight: bold;
            padding: 2vmin 8vmin;
            border-radius: 6vmin;
            box-shadow: 0 5px 0 #8b5a2b;
            cursor: pointer;
            transition: 0.1s;
            border: 2px solid #ffe49e;
        }
        .again-btn:active {
            transform: translateY(5px);
            box-shadow: 0 1px 0 #8b5a2b;
        }
        .mode-indicator {
            background: #2d4a2d;
            padding: 1vmin 4vmin;
            border-radius: 5vmin;
            border: 1px solid #ffd966;
            font-size: 3.5vmin;
            color: #ffdba0;
        }
        .lives-container {
            display: inline-flex;
            align-items: center;
            gap: 1vmin;
            background: #3d2b1c;
            padding: 0.5vmin 3vmin;
            border-radius: 5vmin;
            border: 1px solid #edc27a;
            margin-left: 2vmin;
            font-size: 4vmin;
        }
        .heart {
            color: #ff6b6b;
            filter: drop-shadow(0 0 4px #ffaaaa);
        }
        @media (max-width: 600px) {
            .status-bar { font-size: 6vmin; }
            .stroke-box { padding: 0.8vmin 4vmin; }
            .action-btn { font-size: 4vmin; padding: 0.6vmin 2vmin; }
            .bottom-panel { gap: 1vmin; }
            .power-meter { font-size: 3.5vmin; padding: 0.8vmin 2vmin; }
        }
    </style>
</head>
<body>
<!-- loading overlay -->
<div class="loading-overlay" id="loadingOverlay">
    <div class="spinner"></div>
    <div class="loading-text">Loading Game...</div>
</div>
<!-- splash screen: mode selection -->
<div class="splash-screen" id="splashScreen" style="display: flex;">
    <div class="splash-card">
        <div class="splash-title">🎱 BLACK 8</div>
        <div class="mode-buttons">
            <button class="mode-btn" id="practiceModeBtn">- PRACTICE -</button>
            <button class="mode-btn" id="challengeModeBtn">CHALLENGE</button>
        </div>
        <div class="mode-desc" id="modeDesc">Practice: clear all balls<br>Challenge: 3 lives</div>
    </div>
</div>
<div class="game-container" style="position: relative;">
    <div class="game-header">
        <a href="https://amitofoicu.github.io/home/main.html" target="_blank" rel="noopener noreferrer">
            <img src="https://amitofoicu.github.io/home/logo.jpg" alt="Logo" class="header-logo">
        </a>
        <div class="game-title">🎱 Black 8</div>
        <div class="mode-indicator" id="modeIndicator">PRACTICE</div>
    </div>
    <canvas id="poolCanvas" width="800" height="450"></canvas>
    <div class="hint" id="hintText">
        👆 Drag cue ball · Pull back for power · Release to shoot
    </div>
    <div class="status-bar">
        <!-- dynamic content: practice shows strokes, challenge shows lives -->
        <span id="statusLeftText">Clear all🎱 to win!</span>
        <span id="strokeBox" class="stroke-box">0</span>
        <span id="livesDisplay" class="lives-container" style="display: none;">❤️<span id="livesCount">3</span></span>
    </div>
    <!-- 移除 "Miss strike" 及其所有相关元素:原 challengeCounter 整个div已删除 -->
    <div class="bottom-panel">
        <div class="power-meter">
            <span>💪</span>
            <div class="bar-bg"><div class="bar-fill" id="powerFill" style="width: 20%;"></div></div>
        </div>
        <div class="button-group">
            <button class="action-btn mute" id="muteBtn">🔊</button>
            <button class="action-btn" id="backBtn">🔙 Back</button>  <!-- 改為 Back 按鈕 -->
            <button class="action-btn next" id="nextBtn" style="display: none;">✨ Next</button>
        </div>
    </div>
    <!-- Play again overlay (foul / victory / challenge fail) -->
    <div id="againOverlay" class="again-overlay" style="display: none;">
        <div class="again-card">
            <div class="again-title" id="againMessage">🏆 Play Again</div>
            <button class="again-btn" id="againPlayBtn">🎱 Play Again</button>
        </div>
    </div>
</div>
<script>
    (function() {
        // ----- DOM elements -----
        const canvas = document.getElementById('poolCanvas');
        const ctx = canvas.getContext('2d');
        const strokeBox = document.getElementById('strokeBox');
        const powerFill = document.getElementById('powerFill');
        const nextBtn = document.getElementById('nextBtn');
        const backBtn = document.getElementById('backBtn');  // 改名
        const muteBtn = document.getElementById('muteBtn');
        const loadingOverlay = document.getElementById('loadingOverlay');
        const splashScreen = document.getElementById('splashScreen');
        const modeIndicator = document.getElementById('modeIndicator');
        // 移除了 challengeCounterDiv 和 blackChancesSpan 的获取
        const againOverlay = document.getElementById('againOverlay');
        const againMessage = document.getElementById('againMessage');
        const againPlayBtn = document.getElementById('againPlayBtn');
        const hintText = document.getElementById('hintText');
        const livesDisplay = document.getElementById('livesDisplay');
        const livesCountSpan = document.getElementById('livesCount');
        const statusLeftText = document.getElementById('statusLeftText');
        // ----- mode variables -----
        let gameMode = 'practice';      // 'practice' or 'challenge'
        // 移除 blackMissCount 相关,但保留 lives 逻辑(内部使用变量 blackMissCount 只用于逻辑,不再显示)
        let challengeLives = 3;           // total lives
        // 注意:miss count 不再用于显示,但内部仍可用于判断连击(如果需要可以保留,但为了完全移除GUI,我们把相关显示去掉,内部逻辑简化)
        // 为了简单且不影响原有扣血逻辑,我们保留 blackMissCount 但不展示。
        let blackMissCount = 0;           // 内部记录,不再显示
        // ----- audio management -----
        let isMuted = false;
        let userInteracted = false;
        let hasShot = false; 
        let bgmAudio = null;
        let winAudio = null;          // win.mp3 (black potted)
        let xiaochuAudio = null;      // xiaochu.mp3 (victory fanfare)
        let collisionAudio = null;    // jiqiu.mp3 (ball collision)
        function initAudio() {
            bgmAudio = new Audio('https://amitofoicu.github.io/home/beijing.ogg');
            bgmAudio.loop = true;
            bgmAudio.volume = 1.0;
            bgmAudio.setAttribute('playsinline', '');
            bgmAudio.setAttribute('webkit-playsinline', '');
            bgmAudio.load();
            winAudio = new Audio('https://amitofoicu.github.io/home/win.mp3');
            winAudio.volume = 0.7;
            winAudio.load();
            xiaochuAudio = new Audio('https://amitofoicu.github.io/home/xiaochu.mp3');
            xiaochuAudio.volume = 1.0;
            xiaochuAudio.load();
            collisionAudio = new Audio('https://amitofoicu.github.io/home/jiqiu.mp3');
            collisionAudio.volume = 0.6;
            collisionAudio.load();
            muteBtn.textContent = isMuted ? '🔇' : '🔊';
        }
        function playBGM() {
            if (!hasShot || isMuted || !bgmAudio) return; 
            try {
                if (!bgmAudio.paused) return;
                bgmAudio.currentTime = 0;
                const playPromise = bgmAudio.play();
                if (playPromise !== undefined) playPromise.catch(e => console.log('BGM play failed:', e));
            } catch (e) {}
        }
        function stopBGM() {
            if (!bgmAudio) return;
            try { bgmAudio.pause(); bgmAudio.currentTime = 0; } catch (e) {}
        }
        function playSound(audio) {
            if (!userInteracted || isMuted || !audio) return;
            try {
                const clone = audio.cloneNode();
                clone.volume = audio.volume;
                const playPromise = clone.play();
                if (playPromise !== undefined) playPromise.catch(e => console.log('sound play failed:', e));
                setTimeout(() => { clone.pause(); clone.src = ''; }, 3000);
            } catch (e) {}
        }
        function playWinSound() { playSound(winAudio); }
        function playXiaochuSound() { playSound(xiaochuAudio); }
        function playCollisionSound() { playSound(collisionAudio); }
        function toggleMute() {
            isMuted = !isMuted;
            muteBtn.textContent = isMuted ? '🔇' : '🔊';
            if (isMuted) stopBGM(); else if (hasShot) playBGM();
        }
        function handleUserInteraction() { if (!userInteracted) userInteracted = true; }
        // ----- constants & physics -----
        const CW = 800, CH = 450;
        const LEFT_WALL = 40, RIGHT_WALL = 760, TOP_WALL = 40, BOTTOM_WALL = 410;
        const BALL_RADIUS = 14;
        const FRICTION = 0.98;
        const MAX_POWER_SPEED = 25;
        const pockets = [
            { x: LEFT_WALL, y: TOP_WALL }, { x: RIGHT_WALL, y: TOP_WALL },
            { x: LEFT_WALL, y: BOTTOM_WALL }, { x: RIGHT_WALL, y: BOTTOM_WALL },
            { x: (LEFT_WALL+RIGHT_WALL)/2, y: TOP_WALL }, { x: (LEFT_WALL+RIGHT_WALL)/2, y: BOTTOM_WALL }
        ];
        const POCKET_RADIUS = 28;
        // ----- game state -----
        let white = { x: 200, y: 220, vx: 0, vy: 0 };
        let blackBalls = [];
        const BLACK_COUNT = 7;
        let remainingBlacks = BLACK_COUNT;
        let gameOver = false;
        let winFlag = false;
        let checkWhiteAfterStop = false;
        let strokes = 0;
        let whiteRemoved = false;       // white ball removed (foul)
        // ----- aiming state -----
        let isDragging = false;
        let dragX = 0, dragY = 0;
        let angle = 0;
        let power = 0.2;
        let isMouseOutside = false;
        // ----- Realistic cue animation with spring physics -----
        const CUE_FIXED_LENGTH = 200;           // total length constant
        // cueOffset: positive = pulled back, negative = pushed forward (spring)
        let cueOffset = 0;                       // main animation offset
        let cueVelocity = 0;                      // velocity for smooth physics
        let targetOffset = 0;                     // target based on power
        // Spring constants for realistic feel
        const CUE_SPRING_STRENGTH = 0.2;
        const CUE_DAMPING = 0.92;
        // Flag for shot impulse
        let shotJustFired = false;
        // ----- Challenge mode fix: track if current shot has been processed -----
        let shotProcessedForLives = false;        // 标记本杆是否已处理扣血逻辑
        // ----- particle system (same) -----
        let particles = [];
        const PARTICLE_COLORS = ['#FF69B4','#FFD700','#FF4500','#9370DB','#00FF7F','#FF1493','#FFA500','#32CD32','#FFB6C1','#87CEEB','#FF6346','#FFFF00','#FF00FF','#00FFFF','#FFDAB9'];
        class Particle {
            constructor(x, y, type = 'explosion') {
                this.x = x; this.y = y; this.type = type;
                if (type === 'explosion') {
                    const angle = Math.random() * Math.PI * 2;
                    const speed = Math.random() * 8 + 5;
                    this.vx = Math.cos(angle) * speed;
                    this.vy = Math.sin(angle) * speed;
                    this.size = Math.random() * 10 + 5;
                    this.fadeSpeed = 0.01 + Math.random() * 0.01;
                } else {
                    this.vx = (Math.random() - 0.5) * 1.5;
                    this.vy = Math.random() * 2 + 1.5;
                    this.size = Math.random() * 8 + 4;
                    this.fadeSpeed = 0.001;
                }
                this.color = PARTICLE_COLORS[Math.floor(Math.random() * PARTICLE_COLORS.length)];
                this.rotation = Math.random() * Math.PI * 2;
                this.rotationSpeed = (Math.random() - 0.5) * 0.05;
                this.gravity = 0.05;
                this.life = 1.0;
            }
            update() {
                this.x += this.vx; this.y += this.vy;
                this.vy += this.gravity;
                this.rotation += this.rotationSpeed;
                if (this.type === 'explosion') this.life -= this.fadeSpeed;
                if (this.type === 'rain') {
                    if (this.y > CH + 30) { this.y = -20; this.x = Math.random() * CW; this.vx = (Math.random()-0.5)*1.5; this.vy = Math.random()*2+1.5; }
                    if (this.x < 0 || this.x > CW) this.vx *= -0.8;
                    return true;
                } else {
                    if (this.y > CH + 50) this.life = 0;
                    return this.life > 0;
                }
            }
            draw() {
                ctx.save(); ctx.translate(this.x, this.y); ctx.rotate(this.rotation); ctx.globalAlpha = this.life;
                ctx.beginPath();
                for (let i = 0; i < 5; i++) {
                    let angle = (i/5)*Math.PI*2;
                    let petalLength = this.size, petalWidth = this.size*0.6;
                    let x = Math.cos(angle)*petalLength, y = Math.sin(angle)*petalLength;
                    let cx1 = Math.cos(angle+0.5)*petalWidth, cy1 = Math.sin(angle+0.5)*petalWidth;
                    let cx2 = Math.cos(angle-0.5)*petalWidth, cy2 = Math.sin(angle-0.5)*petalWidth;
                    ctx.moveTo(0,0); ctx.quadraticCurveTo(cx1, cy1, x, y); ctx.quadraticCurveTo(cx2, cy2, 0, 0);
                }
                ctx.fillStyle = this.color; ctx.shadowColor = 'rgba(255, 255, 255, 0.5)'; ctx.shadowBlur = 10; ctx.fill();
                ctx.restore();
            }
        }
        function explodeFlowersFromPocket(pocketX, pocketY, count = 15) {
            for (let i=0; i<count; i++) particles.push(new Particle(pocketX, pocketY, 'explosion'));
        }
        function startRainFlowers(count = 40) {
            particles = particles.filter(p => p.type === 'rain');
            for (let i=0; i<count; i++) particles.push(new Particle(Math.random()*CW, Math.random()*CH-CH, 'rain'));
        }
        function stopRainFlowers() { particles = particles.filter(p => p.type !== 'rain'); }
        function updateParticles() { particles = particles.filter(p => p.update()); }
        function drawParticles() { for (let p of particles) p.draw(); ctx.globalAlpha = 1.0; }
        // ----- helpers -----
        function randomBlackPositions(count, cueX, cueY) {
            let positions = [];
            const minDist = BALL_RADIUS * 2 + 15;
            let attempts = 0, maxAttempts = 5000;
            for (let i=0; i<count; i++) {
                let placed = false;
                while (!placed && attempts < maxAttempts) {
                    attempts++;
                    let newX = LEFT_WALL + Math.random() * (RIGHT_WALL - LEFT_WALL);
                    let newY = TOP_WALL + Math.random() * (BOTTOM_WALL - TOP_WALL);
                    let distCue = Math.hypot(newX - cueX, newY - cueY);
                    if (distCue < minDist) continue;
                    let ok = true;
                    for (let j=0; j<positions.length; j++) {
                        if (Math.hypot(newX - positions[j].x, newY - positions[j].y) < minDist) { ok = false; break; }
                    }
                    if (ok) { positions.push({ x: newX, y: newY, vx:0, vy:0, active:true }); placed = true; }
                }
                if (!placed) positions.push({ x:400+(i*20), y:200+(i*15), vx:0, vy:0, active:true });
            }
            return positions;
        }
        // reset game (keep mode) - 但 Back 按鈕會回到主畫面,所以這個主要用於模式內重啟
        function resetGame() {
            white = { x: 200, y: 220, vx: 0, vy: 0 };
            whiteRemoved = false;
            blackBalls = randomBlackPositions(BLACK_COUNT, white.x, white.y);
            remainingBlacks = BLACK_COUNT;
            gameOver = false;
            winFlag = false;
            checkWhiteAfterStop = false;
            strokes = 0;
            strokeBox.innerText = strokes;
            isDragging = false;
            power = 0.2;
            targetOffset = 0;
            cueOffset = 0;
            cueVelocity = 0;
            powerFill.style.width = '20%';
            nextBtn.style.display = 'none';
            backBtn.style.display = 'inline-block';  // 確保 Back 顯示
            stopRainFlowers();
            againOverlay.style.display = 'none';
            shotJustFired = false;
            shotProcessedForLives = false;
            // reset challenge lives & misses (miss不再显示)
            blackMissCount = 0;
            challengeLives = 3;
            if (gameMode === 'challenge') {
                // 移除了 challengeCounterDiv 的显示,只显示 lives
                livesDisplay.style.display = 'inline-flex';
                livesCountSpan.innerText = challengeLives;
                // hide stroke
                strokeBox.style.display = 'none';
                statusLeftText.innerText = '❤️ Lives left:';
            } else {
                livesDisplay.style.display = 'none';
                strokeBox.style.display = 'inline-block';
                statusLeftText.innerText = 'Clear all🎱 to win! Strokes';
            }
        }
        // 返回主畫面(顯示模式選擇)
        function goBackToSplash() {
            // 停止所有運動
            white.vx = 0; white.vy = 0;
            blackBalls.forEach(b => { if(b.active) { b.vx = 0; b.vy = 0; } });
            isDragging = false;
            // 顯示啟動畫面
            splashScreen.style.display = 'flex';
            splashScreen.style.opacity = '1';
            // 可選:停止背景音樂
            stopBGM();
            hasShot = false;
        }
        // mode selection
        function startGameWithMode(mode) {
            gameMode = mode;
            modeIndicator.innerText = (mode === 'practice') ? 'PRACTICE' : 'CHALLENGE';
            if (mode === 'challenge') {
                // 移除了 challengeCounterDiv 的显示相关代码
                livesDisplay.style.display = 'inline-flex';
                livesCountSpan.innerText = '3';
                strokeBox.style.display = 'none';
                statusLeftText.innerText = '❤️ Lives left:';
                hintText.innerText = '👆 Drag cue ball · Pull back for power · Release to shoot';
            } else {
                livesDisplay.style.display = 'none';
                strokeBox.style.display = 'inline-block';
                statusLeftText.innerText = 'Clear all🎱 to win! Strokes';
                hintText.innerText = '👆 Drag cue ball · Pull back for power · Release to shoot';
            }
            resetGame();
            splashScreen.style.opacity = '0';
            setTimeout(() => splashScreen.style.display = 'none', 400);
        }
        function showAgainOverlay(reason) {
            if (reason === 'foul') againMessage.innerText = '⚪ Cue Ball Foul';
            else if (reason === 'victory') againMessage.innerText = '🏆 VICTORY 🏆';
            else if (reason === 'challengeFail') againMessage.innerText = '❤️ Challenge Failed';
            againOverlay.style.display = 'flex';
        }
        function handleWhiteFoul() {
            whiteRemoved = true;
            white.vx = 0; white.vy = 0;
            blackBalls.forEach(b => { if(b.active) { b.vx = 0; b.vy = 0; } });
            showAgainOverlay('foul');
        }
        function handleChallengeFail() {
            gameOver = true;
            whiteRemoved = true;
            white.vx = white.vy = 0;
            blackBalls.forEach(b => { if(b.active) { b.vx = 0; b.vy = 0; } });
            showAgainOverlay('challengeFail');
        }
        // ----- pocket check -----
        function checkPocket(ball) {
            for (let p of pockets) if (Math.hypot(ball.x-p.x, ball.y-p.y) < POCKET_RADIUS) return p;
            return null;
        }
        let lastCollisionTime = 0;
        const COLLISION_THROTTLE = 80;
        function tryPlayCollisionEffect() {
            const now = Date.now();
            if (now - lastCollisionTime > COLLISION_THROTTLE) { playCollisionSound(); lastCollisionTime = now; }
        }
        function ballsAreStationary() {
            if (!whiteRemoved && (Math.abs(white.vx)>0.1 || Math.abs(white.vy)>0.1)) return false;
            for (let b of blackBalls) if (b.active && (Math.abs(b.vx)>0.1 || Math.abs(b.vy)>0.1)) return false;
            return true;
        }
        let blacksBeforeShot = BLACK_COUNT;
        // ----- physics update (challenge lives logic: miss = lose 1 life) -----
        function updatePhysics() {
            if (whiteRemoved) { 
                updateParticles(); 
                return; 
            }
            if (!gameOver || checkWhiteAfterStop) {
                white.x += white.vx; white.y += white.vy;
                for (let b of blackBalls) if (b.active) { b.x += b.vx; b.y += b.vy; }
                // wall collisions
                if (white.x - BALL_RADIUS < LEFT_WALL) { white.x = LEFT_WALL + BALL_RADIUS; white.vx = -white.vx * 0.92; }
                if (white.x + BALL_RADIUS > RIGHT_WALL) { white.x = RIGHT_WALL - BALL_RADIUS; white.vx = -white.vx * 0.92; }
                if (white.y - BALL_RADIUS < TOP_WALL) { white.y = TOP_WALL + BALL_RADIUS; white.vy = -white.vy * 0.92; }
                if (white.y + BALL_RADIUS > BOTTOM_WALL) { white.y = BOTTOM_WALL - BALL_RADIUS; white.vy = -white.vy * 0.92; }
                for (let b of blackBalls) {
                    if (!b.active) continue;
                    if (b.x - BALL_RADIUS < LEFT_WALL) { b.x = LEFT_WALL + BALL_RADIUS; b.vx = -b.vx * 0.92; }
                    if (b.x + BALL_RADIUS > RIGHT_WALL) { b.x = RIGHT_WALL - BALL_RADIUS; b.vx = -b.vx * 0.92; }
                    if (b.y - BALL_RADIUS < TOP_WALL) { b.y = TOP_WALL + BALL_RADIUS; b.vy = -b.vy * 0.92; }
                    if (b.y + BALL_RADIUS > BOTTOM_WALL) { b.y = BOTTOM_WALL - BALL_RADIUS; b.vy = -b.vy * 0.92; }
                }
                // white vs black
                for (let b of blackBalls) {
                    if (!b.active) continue;
                    const dx = b.x - white.x, dy = b.y - white.y, dist = Math.hypot(dx, dy);
                    if (dist < BALL_RADIUS*2 && dist > 0.001) {
                        const nx = dx/dist, ny = dy/dist;
                        const vrelx = white.vx - b.vx, vrely = white.vy - b.vy, vn = vrelx*nx + vrely*ny;
                        if (vn > 0) { const imp = (2*vn)/2*0.96; white.vx -= imp*nx; white.vy -= imp*ny; b.vx += imp*nx; b.vy += imp*ny; tryPlayCollisionEffect(); }
                        const overlap = BALL_RADIUS*2 - dist;
                        if (overlap > 0) { const sepX = nx*overlap*0.5, sepY = ny*overlap*0.5; white.x -= sepX; white.y -= sepY; b.x += sepX; b.y += sepY; }
                    }
                }
                // black vs black
                for (let i=0; i<blackBalls.length; i++) {
                    if (!blackBalls[i].active) continue;
                    for (let j=i+1; j<blackBalls.length; j++) {
                        if (!blackBalls[j].active) continue;
                        const b1=blackBalls[i], b2=blackBalls[j];
                        const dx = b2.x-b1.x, dy = b2.y-b1.y, dist = Math.hypot(dx,dy);
                        if (dist < BALL_RADIUS*2 && dist > 0.001) {
                            const nx = dx/dist, ny = dy/dist;
                            const vrelx = b1.vx - b2.vx, vrely = b1.vy - b2.vy, vn = vrelx*nx + vrely*ny;
                            if (vn > 0) { const imp = (2*vn)/2*0.96; b1.vx -= imp*nx; b1.vy -= imp*ny; b2.vx += imp*nx; b2.vy += imp*ny; tryPlayCollisionEffect(); }
                            const overlap = BALL_RADIUS*2 - dist;
                            if (overlap > 0) { const sepX = nx*overlap*0.5, sepY = ny*overlap*0.5; b1.x -= sepX; b1.y -= sepY; b2.x += sepX; b2.y += sepY; }
                        }
                    }
                }
                white.vx *= FRICTION; white.vy *= FRICTION;
                for (let b of blackBalls) if (b.active) { b.vx *= FRICTION; b.vy *= FRICTION; }
                if (Math.abs(white.vx) < 0.1) white.vx = 0;
                if (Math.abs(white.vy) < 0.1) white.vy = 0;
                for (let b of blackBalls) if (b.active) { if (Math.abs(b.vx) < 0.1) b.vx = 0; if (Math.abs(b.vy) < 0.1) b.vy = 0; }
                // black pockets
                for (let b of blackBalls) {
                    if (!b.active) continue;
                    const pocket = checkPocket(b);
                    if (pocket) {
                        b.active = false;
                        remainingBlacks--;
                        playWinSound();
                        explodeFlowersFromPocket(pocket.x, pocket.y, 12);
                    }
                }
                // white pocket → foul
                if (checkPocket(white)) { handleWhiteFoul(); return; }
                if (remainingBlacks === 0 && !winFlag && !checkWhiteAfterStop) {
                    checkWhiteAfterStop = true;
                }
            }
            if (checkWhiteAfterStop && ballsAreStationary()) {
                if (checkPocket(white)) {
                    handleWhiteFoul();
                } else {
                    playXiaochuSound();
                    gameOver = true; winFlag = true;
                    startRainFlowers(50);
                    showAgainOverlay('victory');
                    nextBtn.style.display = 'none';
                    backBtn.style.display = 'inline-block'; // 保持 Back 可見
                }
                checkWhiteAfterStop = false;
            }
            // ----- 修复挑战模式扣血逻辑:使用 shotProcessedForLives 确保每杆只处理一次 -----
            if (gameMode === 'challenge' && !whiteRemoved && ballsAreStationary() && strokes > 0 && !gameOver && !winFlag) {
                // 只有当本杆尚未处理时,才进入判断
                if (!shotProcessedForLives) {
                    let currentBlacks = blackBalls.filter(b => b.active).length;
                    if (currentBlacks === blacksBeforeShot) {
                        // 没有黑球入袋:扣血
                        challengeLives--;
                        if (challengeLives < 0) challengeLives = 0;
                        livesCountSpan.innerText = challengeLives;
                        // 内部miss计数保留但不再显示
                        blackMissCount++;
                        if (challengeLives <= 0) {
                            handleChallengeFail();
                        }
                    } else {
                        // 有黑球入袋:重置连击计数
                        blackMissCount = 0;
                        // 生命值不变
                    }
                    // 标记本杆已处理,防止重复扣血
                    shotProcessedForLives = true;
                }
            }
            // Realistic cue animation with spring physics
            if (isDragging) {
                // Map power 0.2-1.2 to offset 0-120 (pulled back)
                targetOffset = (power - 0.2) * 120; // max 120 pullback
            } else {
                targetOffset = 0;
            }
            // Spring physics for smooth, realistic motion
            let force = (targetOffset - cueOffset) * CUE_SPRING_STRENGTH;
            cueVelocity += force;
            cueVelocity *= CUE_DAMPING;
            cueOffset += cueVelocity;
            // Apply shot impulse if shot just fired
            if (shotJustFired) {
                cueVelocity += -25; // Strong forward impulse
                shotJustFired = false;
            }
            // Limit extreme offsets
            if (cueOffset > 150) cueOffset = 150;
            if (cueOffset < -50) cueOffset = -50;
            updateParticles();
        }
        // ----- shoot (trigger spring release) -----
        function shoot() {
            if (gameOver || whiteRemoved) return;
            if (white.vx !== 0 || white.vy !== 0) return;
            if (remainingBlacks === 0) return;
            // Set flag for forward impulse on next physics update
            shotJustFired = true;
            const speed = power * MAX_POWER_SPEED;
            white.vx = Math.cos(angle) * speed;
            white.vy = Math.sin(angle) * speed;
            strokes++;
            strokeBox.innerText = strokes;
            blacksBeforeShot = blackBalls.filter(b => b.active).length;
            // 新的一杆,重置扣血处理标记
            shotProcessedForLives = false;
            if (!hasShot) { hasShot = true; setTimeout(() => { if (!isMuted) playBGM(); }, 200); }
        }
        function updateAim() {
            if (!isDragging) return;
            const dx = white.x - dragX, dy = white.y - dragY;
            let dist = Math.hypot(dx, dy);
            if (dist < 1) return;
            angle = Math.atan2(dy, dx);
            const MIN_POWER = 0.2, MAX_POWER = 1.2, OPTIMAL_DIST = 180;
            let rawPower = dist / OPTIMAL_DIST;
            if (rawPower < 0.5) power = MIN_POWER + (rawPower/0.5)*0.3;
            else if (rawPower < 1.2) power = MIN_POWER + 0.3 + ((rawPower-0.5)/0.7)*0.5;
            else power = MIN_POWER + 0.8 + Math.min(rawPower-1.2,0.5)*0.4;
            power = Math.max(MIN_POWER, Math.min(MAX_POWER, power));
            let visualPower = Math.min(power, 1.0);
            powerFill.style.width = (visualPower * 100) + '%';
        }
        // ----- enhanced aiming: cue stick & collision prediction -----
        function predictCollision() {
            const dirX = Math.cos(angle);
            const dirY = Math.sin(angle);
            const startX = white.x + dirX * BALL_RADIUS;
            const startY = white.y + dirY * BALL_RADIUS;
            let closestHit = null;
            let minDist = Infinity;
            for (let b of blackBalls) {
                if (!b.active) continue;
                const tx = b.x;
                const ty = b.y;
                const toTargetX = tx - startX;
                const toTargetY = ty - startY;
                const proj = toTargetX * dirX + toTargetY * dirY;
                if (proj < 0) continue;
                const closestX = startX + dirX * proj;
                const closestY = startY + dirY * proj;
                const perpDist = Math.hypot(tx - closestX, ty - closestY);
                if (perpDist > BALL_RADIUS * 2) continue;
                const hitDist = proj - Math.sqrt(Math.max(0, Math.pow(BALL_RADIUS * 2, 2) - perpDist * perpDist));
                if (hitDist < 0) continue;
                if (hitDist < minDist) {
                    minDist = hitDist;
                    closestHit = { hitX: startX + dirX * hitDist, hitY: startY + dirY * hitDist };
                }
            }
            return closestHit;
        }
        // ----- draw with realistic cue animation -----
        function draw() {
            ctx.clearRect(0,0,CW,CH);
            ctx.fillStyle='#1e5a3a'; ctx.fillRect(0,0,CW,CH);
            ctx.strokeStyle='#dbb06b'; ctx.lineWidth=3; ctx.strokeRect(LEFT_WALL-2,TOP_WALL-2,RIGHT_WALL-LEFT_WALL+4,BOTTOM_WALL-TOP_WALL+4);
            ctx.shadowColor='#00000080'; ctx.shadowBlur=10;
            pockets.forEach(p=>{ ctx.beginPath(); ctx.arc(p.x,p.y,POCKET_RADIUS-4,0,Math.PI*2); ctx.fillStyle='#1f140e'; ctx.fill(); ctx.shadowBlur=5; ctx.fillStyle='#4a3c2b'; ctx.arc(p.x,p.y,POCKET_RADIUS-8,0,Math.PI*2); ctx.fill(); });
            ctx.shadowBlur=0;
            for (let b of blackBalls) if(b.active) {
                ctx.shadowColor='#333'; ctx.shadowBlur=15; ctx.beginPath(); ctx.arc(b.x,b.y,BALL_RADIUS,0,Math.PI*2);
                const grad=ctx.createRadialGradient(b.x-3,b.y-3,3,b.x,b.y,BALL_RADIUS+2); grad.addColorStop(0,'#222'); grad.addColorStop(0.7,'#000'); ctx.fillStyle=grad; ctx.fill();
                ctx.shadowBlur=5; ctx.font='bold 16px "Segoe UI",Arial'; ctx.fillStyle='white'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('8',b.x,b.y);
                ctx.beginPath(); ctx.arc(b.x-3,b.y-3,4,0,Math.PI*2); ctx.fillStyle='#fff9e6'; ctx.globalAlpha=0.3; ctx.fill(); ctx.globalAlpha=1;
            }
            if (!whiteRemoved) {
                ctx.shadowColor='#ccc'; ctx.shadowBlur=18; ctx.beginPath(); ctx.arc(white.x,white.y,BALL_RADIUS,0,Math.PI*2);
                const wgrad=ctx.createRadialGradient(white.x-4,white.y-4,4,white.x,white.y,BALL_RADIUS+2); wgrad.addColorStop(0,'#fafaf5'); wgrad.addColorStop(0.8,'#c0c0c0'); ctx.fillStyle=wgrad; ctx.fill();
                ctx.shadowBlur=8; ctx.beginPath(); ctx.arc(white.x-1,white.y-1,5,0,Math.PI*2); ctx.fillStyle='#22222260'; ctx.fill();
            }
            // Aiming guide & cue stick (only when white exists, not moving)
            if (!whiteRemoved && isDragging && white.vx === 0 && white.vy === 0 && remainingBlacks > 0 && !gameOver) {
                // draw power ring at drag position
                let visualPower = Math.min(power, 1.0);
                ctx.beginPath();
                ctx.arc(dragX, dragY, 15 + visualPower * 20, 0, Math.PI * 2);
                ctx.strokeStyle = 'rgba(255, 200, 0, 0.5)';
                ctx.lineWidth = 3;
                ctx.stroke();
                // dashed guide line from white through drag
                ctx.setLineDash([8, 6]);
                ctx.beginPath();
                ctx.moveTo(white.x, white.y);
                ctx.lineTo(white.x + Math.cos(angle) * 500, white.y + Math.sin(angle) * 500);
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
                ctx.stroke();
                ctx.setLineDash([]);
                // Draw cue with spring animation
                const cueLength = CUE_FIXED_LENGTH + cueOffset;
                const backX = white.x - Math.cos(angle) * cueLength;
                const backY = white.y - Math.sin(angle) * cueLength;
                // cue shaft with gradient for 3D effect
                ctx.shadowBlur = 15;
                ctx.shadowColor = '#222';
                ctx.beginPath();
                ctx.moveTo(backX, backY);
                ctx.lineTo(white.x, white.y);
                // Create gradient along cue
                const gradient = ctx.createLinearGradient(backX, backY, white.x, white.y);
                gradient.addColorStop(0, '#8B5A2B');
                gradient.addColorStop(0.5, '#B08D57');
                gradient.addColorStop(1, '#D2B48C');
                ctx.strokeStyle = gradient;
                ctx.lineWidth = 12;
                ctx.stroke();
                // Add cue wrap pattern
                ctx.beginPath();
                ctx.moveTo(backX + (white.x-backX)*0.3, backY + (white.y-backY)*0.3);
                ctx.lineTo(backX + (white.x-backX)*0.35, backY + (white.y-backY)*0.35);
                ctx.strokeStyle = '#654321';
                ctx.lineWidth = 2;
                ctx.stroke();
                // cue tip (ferrule)
                ctx.beginPath();
                ctx.arc(white.x + Math.cos(angle) * 4, white.y + Math.sin(angle) * 4, 5, 0, 2*Math.PI);
                ctx.fillStyle = '#DEB887';
                ctx.shadowBlur = 12;
                ctx.fill();
                // white leather tip
                ctx.beginPath();
                ctx.arc(white.x + Math.cos(angle) * 8, white.y + Math.sin(angle) * 8, 3, 0, 2*Math.PI);
                ctx.fillStyle = '#F5DEB3';
                ctx.fill();
                // draw collision prediction marker
                const hit = predictCollision();
                if (hit) {
                    ctx.beginPath();
                    ctx.arc(hit.hitX, hit.hitY, 8, 0, Math.PI * 2);
                    ctx.fillStyle = '#FFD966';
                    ctx.shadowColor = 'black';
                    ctx.shadowBlur = 12;
                    ctx.fill();
                }
                ctx.shadowBlur = 0;
                ctx.setLineDash([]);
            }
            drawParticles();
            if (winFlag) {
                ctx.fillStyle='rgba(0,0,0,0.3)'; ctx.fillRect(0,0,CW,CH);
                ctx.shadowBlur=30; ctx.font='bold 52px "Segoe UI",Verdana'; ctx.fillStyle='#FFD966'; ctx.strokeStyle='#8B4513'; ctx.lineWidth=6; ctx.textAlign='center';
                ctx.strokeText('🎉 VICTORY!',CW/2,150); ctx.fillText('🎉 VICTORY!',CW/2,150);
            }
        }
        // ----- event listeners (same) -----
        function getCanvasCoords(e) {
            const rect = canvas.getBoundingClientRect();
            const scaleX = canvas.width / rect.width, scaleY = canvas.height / rect.height;
            let clientX, clientY;
            if (e.touches) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; }
            else { clientX = e.clientX; clientY = e.clientY; }
            return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };
        }
        function onMouseDown(e){ e.preventDefault(); handleUserInteraction(); if(gameOver||whiteRemoved||white.vx!==0||white.vy!==0||remainingBlacks===0) return; const c=getCanvasCoords(e); dragX=c.x; dragY=c.y; isDragging=true; }
        function onMouseMove(e){ e.preventDefault(); if(!isDragging) return; const c=getCanvasCoords(e); dragX=c.x; dragY=c.y; updateAim(); }
        function onMouseUp(e){ e.preventDefault(); if(!isDragging) return; isDragging=false; shoot(); }
        canvas.addEventListener('mousedown',onMouseDown);
        canvas.addEventListener('mousemove',onMouseMove);
        canvas.addEventListener('mouseup',onMouseUp);
        canvas.addEventListener('touchstart',(e)=>{ e.preventDefault(); handleUserInteraction(); if(gameOver||whiteRemoved||white.vx!==0||white.vy!==0||remainingBlacks===0) return; const c=getCanvasCoords(e); dragX=c.x; dragY=c.y; isDragging=true; },{passive:false});
        canvas.addEventListener('touchmove',(e)=>{ e.preventDefault(); if(!isDragging) return; const c=getCanvasCoords(e); dragX=c.x; dragY=c.y; updateAim(); },{passive:false});
        canvas.addEventListener('touchend',(e)=>{ e.preventDefault(); if(!isDragging) return; isDragging=false; shoot(); });
        window.addEventListener('mousemove',(e)=>{ if(isDragging){ const c=getCanvasCoords(e); dragX=c.x; dragY=c.y; updateAim(); } });
        window.addEventListener('mouseup',(e)=>{ if(isDragging){ isDragging=false; shoot(); } });
        // Back 按鈕:返回主畫面
        backBtn.addEventListener('click', (e) => {
            e.preventDefault();
            handleUserInteraction();
            goBackToSplash();
        });
        // 注意:原本的 restartBtn 已經移除,所以沒有衝突
        // nextBtn 保留(但預設隱藏)
        nextBtn.addEventListener('click', (e) => {
            e.preventDefault();
            handleUserInteraction();
            resetGame();  // 如果有的話,還是重置當前模式
        });
        muteBtn.addEventListener('click', (e) => {
            e.preventDefault();
            handleUserInteraction();
            toggleMute();
        });
        againPlayBtn.addEventListener('click', (e) => {
            e.preventDefault();
            handleUserInteraction();
            resetGame();
        });
        document.addEventListener('touchend', (e) => { 
            const now = Date.now(); 
            if (now - lastTouchEnd <= 300) e.preventDefault(); 
            lastTouchEnd = now; 
        }, false);
        document.addEventListener('contextmenu', e => e.preventDefault());
        document.body.addEventListener('touchmove', (e) => { 
            if (e.target === document.body) e.preventDefault(); 
        }, { passive: false });
        // mode selection buttons
        document.getElementById('practiceModeBtn').addEventListener('click', ()=>{
            startGameWithMode('practice');
        });
        document.getElementById('challengeModeBtn').addEventListener('click', ()=>{
            startGameWithMode('challenge');
        });
        // initialization
        initAudio();
        setTimeout(() => {
            loadingOverlay.style.opacity = '0';
            setTimeout(() => {
                loadingOverlay.style.display = 'none';
            }, 500);
        }, 1000);
        // 確保 splash 顯示
        splashScreen.style.display = 'flex';
        splashScreen.style.opacity = '1';
        let lastTouchEnd = 0;
        let victoryTimer = null;
        function animate() {
            updatePhysics();
            draw();
            requestAnimationFrame(animate);
        }
        animate();
    })();
</script>
</body>
</html>
© 版权声明

相关文章