云计算百科
云计算领域专业知识百科平台

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

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

主要功能提升:

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

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

<!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://amitofoicu.github.io/home/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>

赞(0)
未经允许不得转载:网硕互联帮助中心 » DeepSeek深度训练的网页小游戏 -- Black 8(高级版)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!