Spaces:
Running
Running
| <html lang="zh-Hant"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>數學探險島 - 海風港灣</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;500;700&display=swap" rel="stylesheet"> | |
| <!-- MathJax for rendering formulas --> | |
| <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script> | |
| <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script> | |
| <style> | |
| body { | |
| font-family: 'Noto Sans TC', sans-serif; | |
| overflow: hidden; | |
| background-image: url('https://i.meee.com.tw/TDC4EZE.png'); | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| #game-container-bg { | |
| background-image: url('https://i.meee.com.tw/vT2QeZF.png'); | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| .seaweed { | |
| position: absolute; | |
| bottom: -10px; | |
| pointer-events: none; | |
| filter: brightness(0.9); | |
| } | |
| .fish { | |
| position: absolute; | |
| cursor: pointer; | |
| transition: transform 0.2s ease, opacity 0.15s ease; | |
| } | |
| .fish:hover { | |
| transform: scale(1.1); | |
| } | |
| .catch-feedback { | |
| position: absolute; | |
| font-size: 2rem; | |
| font-weight: bold; | |
| pointer-events: none; | |
| animation: fadeUp 1s forwards; | |
| text-shadow: 1px 1px 2px black; | |
| } | |
| @keyframes fadeUp { | |
| from { opacity: 1; transform: translateY(0); } | |
| to { opacity: 0; transform: translateY(-50px); } | |
| } | |
| .loader { | |
| border: 8px solid #f3f3f3; | |
| border-radius: 50%; | |
| border-top: 8px solid #3498db; | |
| width: 60px; | |
| height: 60px; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .market-choice.selected { | |
| border-color: #facc15; /* yellow-400 */ | |
| box-shadow: 0 0 15px #facc15; | |
| } | |
| .secret-bg { | |
| background-image: url('https://i.meee.com.tw/EKZpYKI.png'); | |
| background-size: cover; | |
| background-position: center; | |
| } | |
| </style> | |
| </head> | |
| <body class="flex items-center justify-center h-screen"> | |
| <div id="container" class="w-full max-w-4xl mx-auto text-white p-4 flex flex-col h-[90vh] max-h-[800px] relative"> | |
| <!-- 引導畫面 --> | |
| <div id="start-screen" class="flex flex-col items-center justify-center text-center p-8 bg-black bg-opacity-70 rounded-lg my-auto"> | |
| <h1 class="text-5xl font-bold text-cyan-300 mb-4" style="text-shadow: 2px 2px 4px #000;">海風港灣</h1> | |
| <p class="text-xl text-gray-200 mb-8 max-w-2xl">歡迎來到海風港灣!這次的任務是捕捉指定的魚種,並到市場賣出。有了捕捉的技術,真正要賺大錢還得靠數學呢!請你在捕捉完漁獲後,根據市場需求,將漁獲賣到最適合的市場!</p> | |
| <button id="start-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-4 px-8 rounded-lg text-2xl"> | |
| 開始抓魚 | |
| </button> | |
| </div> | |
| <!-- 載入畫面 --> | |
| <div id="loading-screen" class="hidden flex-col items-center justify-center text-center p-8 bg-black bg-opacity-70 rounded-lg my-auto"> | |
| <div class="loader mb-6"></div> | |
| <p id="loading-text" class="text-2xl text-gray-200">正在準備海底世界...</p> | |
| </div> | |
| <!-- 遊戲畫面 --> | |
| <div id="game-screen" class="hidden flex-col h-full w-full"> | |
| <div class="flex justify-between items-center p-2 bg-black/50 rounded-t-lg"> | |
| <div> | |
| <p class="text-lg">目標: <span id="goal-text" class="font-bold text-yellow-400"></span></p> | |
| <p class="text-lg">已捕捉: <span id="score" class="font-bold text-xl">0</span> / <span id="goal-count"></span></p> | |
| </div> | |
| <p class="text-sm text-cyan-200">請適度捕撈,維持海洋永續</p> | |
| <div> | |
| <p class="text-lg">時間</p> | |
| <p id="timer" class="font-bold text-3xl">20</p> | |
| </div> | |
| </div> | |
| <div id="game-container-bg" class="relative flex-grow rounded-b-lg overflow-hidden border-4 border-black/50"> | |
| <div id="game-container" class="absolute inset-0"> | |
| <img src="https://i.meee.com.tw/XE9mkQQ.gif" class="seaweed" style="left: 0%; height: 45%; transform: scaleX(-1);" alt="[海草的Image]"> | |
| <img src="https://i.meee.com.tw/cTBZUgx.gif" class="seaweed" style="left: 20%; height: 35%;" alt="[海草的Image]"> | |
| <img src="https://i.meee.com.tw/KyN2GaF.gif" class="seaweed" style="left: 45%; height: 30%;" alt="[海草的Image]"> | |
| <img src="https://i.meee.com.tw/XE9mkQQ.gif" class="seaweed" style="right: 25%; height: 48%;" alt="[海草的Image]"> | |
| <img src="https://i.meee.com.tw/cTBZUgx.gif" class="seaweed" style="right: 10%; height: 55%; transform: scaleX(-1);" alt="[海草的Image]"> | |
| <img src="https://i.meee.com.tw/KyN2GaF.gif" class="seaweed" style="right: -2%; height: 38%;" alt="[海草的Image]"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 疊加畫面 (操作說明, 成功/失敗) --> | |
| <div id="overlay-container" class="hidden absolute inset-0 flex-col items-center justify-center z-10"> | |
| <div id="instructions-screen" class="hidden bg-black bg-opacity-80 w-full h-full flex-col items-center justify-center text-center p-4"> | |
| <div id="instructions-content" class="bg-slate-800 p-8 rounded-lg"> | |
| <!-- JS will fill this --> | |
| </div> | |
| </div> | |
| <div id="game-over-screen" class="hidden bg-black bg-opacity-70 w-full h-full flex-col items-center justify-center text-center p-4"> | |
| <h2 class="text-4xl font-bold text-red-500 mb-4">挑戰失敗!</h2> | |
| <button id="restart-button" class="bg-amber-500 hover:bg-amber-600 text-gray-900 font-bold py-3 px-6 rounded-lg text-xl"> | |
| 重新挑戰 | |
| </button> | |
| </div> | |
| <div id="win-screen" class="hidden bg-black bg-opacity-70 w-full h-full flex-col items-center justify-center text-center p-4"> | |
| <h2 class="text-4xl font-bold text-green-400 mb-4">成功!</h2> | |
| <p class="text-xl mb-6">你抓到足夠的漁獲了!</p> | |
| <button id="continue-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-6 rounded-lg text-xl"> | |
| 繼續前進 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 市場選擇畫面 --> | |
| <div id="market-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-70 rounded-lg"> | |
| <div id="market-instructions" class="text-center my-auto"> | |
| <h2 class="text-3xl font-bold text-amber-300 mb-6">任務說明:選擇市場</h2> | |
| <p class="text-lg">每個地點對<span class="market-fish-name text-yellow-300 font-bold"></span>的需求都不同,需求<span class="text-yellow-300 font-bold">比例</span>越高的市場,就能賣到越好的價格!</p> | |
| <p class="text-lg mt-2">仔細觀察下方的統計圖,找出最賺錢的市場吧!</p> | |
| <button id="show-market-choices-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8"> | |
| 了解,前往選擇 | |
| </button> | |
| </div> | |
| <div id="market-choices" class="hidden flex-col"> | |
| <h2 class="text-3xl font-bold text-amber-300 text-center mb-4">你要把<span class="market-fish-name"></span>賣到哪裡?</h2> | |
| <div id="market-images-container" class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <!-- Market images will be inserted here by JS --> | |
| </div> | |
| <p id="market-feedback" class="text-center text-xl font-bold min-h-[32px] mt-4"></p> | |
| <button id="next-level-button" class="hidden bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg text-xl w-full max-w-xs mx-auto mt-4"> | |
| <!-- Button text will be set by JS --> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 攻略畫面 --> | |
| <div id="strategy-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-80 rounded-lg text-left overflow-y-auto"> | |
| <h1 class="text-3xl font-bold text-amber-300 text-center mb-4">經營之神的攻略</h1> | |
| <p class="text-lg mb-4">在真實世界中,不同地點的調查所收到的資料不一定會一樣多,總人數不一樣的時候,我們很難用肉眼就判斷出比例的高低,因此需要「相對次數」這樣的統計數據。</p> | |
| <div class="text-center bg-gray-900 p-3 rounded-lg text-xl mb-4"> | |
| $$\text{相對次數} = \frac{\text{需求(人)}}{\text{總人數}} \times 100\%$$ | |
| </div> | |
| <img src="https://i.meee.com.tw/i67LLqV.png" class="rounded-lg mx-auto my-4 w-full max-w-md" alt="[包含相對次數的統計圖表]"> | |
| <p class="text-lg mb-6 text-center">有了相對次數,會不會更好判斷呢?用下一關的挑戰來試試吧!</p> | |
| <button id="start-final-stage-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl w-full max-w-xs mx-auto mt-4"> | |
| 挑戰最終關卡 | |
| </button> | |
| </div> | |
| <!-- 最終秘密畫面 --> | |
| <div id="final-secret-screen" class="hidden flex-col h-full w-full p-6 bg-black bg-opacity-80 rounded-lg text-left overflow-y-auto"> | |
| <h1 class="text-3xl font-bold text-amber-300 text-center mb-6">海風港灣的秘密</h1> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> | |
| <img src="https://i.meee.com.tw/rmcY2Vc.png" class="rounded-lg border-4 border-amber-400" alt="[代數之丘的統計圖表]"> | |
| <img src="https://i.meee.com.tw/qV8QrUI.png" class="rounded-lg" alt="[哲學之塔的統計圖表]"> | |
| <img src="https://i.meee.com.tw/i67LLqV.png" class="rounded-lg" alt="[寶石洞窟的統計圖表]"> | |
| </div> | |
| <p class="text-lg mb-4">恭喜你,完成了所有挑戰!你學會了捕魚,也學會了如何分析數據來做出最好的商業決策。</p> | |
| <p class="text-2xl font-bold text-cyan-300 mb-4">這個港灣的秘密就是:<br>「數字」本身有時候會騙人,「比例」才能揭露真相。</p> | |
| <p class="text-lg mb-4">單看數字,哲學之塔似乎是更好的市場。但當你把「總人數」也考慮進來,計算出「相對次數」(也就是需求比例),你才會發現代數之丘的需求比例,是超過哲學之塔的。</p> | |
| <p class="text-lg">這個智慧,不只適用於賣魚。未來在你看新聞、分析報告,甚至做人生重大決定時,記得問自己:「這個數字背後的『分母』是什麼?」看透比例,你就能做出更聰明的選擇。</p> | |
| <a href="index.html" id="back-to-map-button" class="inline-block text-center bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-8 rounded-lg text-xl w-full max-w-xs mx-auto mt-8"> | |
| 回到探險島地圖 | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Image Zoom Modal --> | |
| <div id="zoom-modal" class="hidden fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4 cursor-pointer"> | |
| <img id="zoomed-image" src="" alt="放大的統計圖表" class="max-w-full max-h-full rounded-lg shadow-2xl"> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // --- 畫面元素 --- | |
| const startScreen = document.getElementById('start-screen'); | |
| const loadingScreen = document.getElementById('loading-screen'); | |
| const loadingText = document.getElementById('loading-text'); | |
| const gameScreen = document.getElementById('game-screen'); | |
| const startButton = document.getElementById('start-button'); | |
| const gameContainer = document.getElementById('game-container'); | |
| const scoreDisplay = document.getElementById('score'); | |
| const timerDisplay = document.getElementById('timer'); | |
| const instructionsScreen = document.getElementById('instructions-screen'); | |
| const instructionsContent = document.getElementById('instructions-content'); | |
| const goalText = document.getElementById('goal-text'); | |
| const goalCount = document.getElementById('goal-count'); | |
| const overlayContainer = document.getElementById('overlay-container'); | |
| const gameOverScreen = document.getElementById('game-over-screen'); | |
| const winScreen = document.getElementById('win-screen'); | |
| const restartButton = document.getElementById('restart-button'); | |
| const continueButton = document.getElementById('continue-button'); | |
| const marketScreen = document.getElementById('market-screen'); | |
| const marketInstructions = document.getElementById('market-instructions'); | |
| const marketChoices = document.getElementById('market-choices'); | |
| const showMarketChoicesButton = document.getElementById('show-market-choices-button'); | |
| const marketImagesContainer = document.getElementById('market-images-container'); | |
| const marketFeedback = document.getElementById('market-feedback'); | |
| const nextLevelButton = document.getElementById('next-level-button'); | |
| const startFinalStageButton = document.getElementById('start-final-stage-button'); | |
| const finalSecretScreen = document.getElementById('final-secret-screen'); | |
| const zoomModal = document.getElementById('zoom-modal'); | |
| const zoomedImage = document.getElementById('zoomed-image'); | |
| // --- 遊戲設定 --- | |
| const MAX_FISH_ON_SCREEN = 8; | |
| let score = 0, timeLeft = 0; | |
| let gameInterval, timerInterval, fishSpawnInterval; | |
| let currentLevel = 0; | |
| let currentLevelSettings; | |
| const levels = [ | |
| { goal: 10, time: 20, targetFish: '小丑魚', speedMultiplier: 1.0, spawnRate: 1200, fishSet: ['小丑魚', '螃蟹'] }, | |
| { goal: 10, time: 20, targetFish: '水母', speedMultiplier: 1.5, spawnRate: 1000, fishSet: ['水母', '章魚'] }, | |
| { goal: 10, time: 20, targetFish: '水母', speedMultiplier: 1.8, spawnRate: 800, fishSet: ['水母', '鯊魚'] } | |
| ]; | |
| const fishTypes = [ | |
| { name: '小丑魚', src: 'https://i.meee.com.tw/7tfldTT.gif', width: 160, height: 120 }, | |
| { name: '鯊魚', src: 'https://i.meee.com.tw/KCQk7C6.gif', width: 270, height: 270 }, | |
| { name: '章魚', src: 'https://i.meee.com.tw/lBWJmMy.gif', width: 160, height: 130 }, | |
| { name: '海馬', src: 'https://i.meee.com.tw/sMehyty.gif', width: 150, height: 150 }, | |
| { name: '螃蟹', src: 'https://i.meee.com.tw/zx9Vg0B.gif', width: 150, height: 110 }, | |
| { name: '水母', src: 'https://i.meee.com.tw/81gncdG.gif', width: 160, height: 160 }, | |
| ]; | |
| const marketLevels = [ | |
| { | |
| images: { A: 'https://i.meee.com.tw/jQ3A90u.png', B: 'https://i.meee.com.tw/HEx1GWE.png', C: 'https://i.meee.com.tw/199bHzA.png' }, | |
| correctAnswer: 'B', | |
| feedback: { correct: "答對了!哲學之塔的需求比例最高,要賺大錢還是得靠數學,不能蠻幹阿!", wrong: "每個地點的調查人數都一樣多,需求數量越多,即表示需求比例越高!" } | |
| }, | |
| { | |
| // A: 代數之丘(左), B: 哲學之塔(中), C: 寶石洞窟(右) | |
| images: { A: 'https://i.meee.com.tw/lTXgvxv.png', B: 'https://i.meee.com.tw/FKPNhKh.png', C: 'https://i.meee.com.tw/zdv5MtZ.png' }, | |
| correctAnswer: 'A', // 正確答案是代數之丘 | |
| feedback: { | |
| firstClick: { | |
| C: "你確定嗎?其他兩個地方的需求人數比較多喔!若是確定請再點選一次", | |
| B: "你確定嗎?雖然哲學之塔的需求人數最多,但他的總人數也最多喔!你確定他是比例最高的嗎?", | |
| A: "你確定嗎?哲學之塔的需求人數比較多喔!" | |
| }, | |
| correct: "你做出了正確的決定!但到底是運氣好,還是數學好呢,如果是運氣好的話,可沒有辦法長久經營阿~", | |
| wrong: "商品買賣,不是越多人買越賺錢,不是喔!因為你長期的決策錯誤,導致買賣虧損,最後造成店家倒閉..." | |
| } | |
| }, | |
| { | |
| // A: 代數之丘, B: 哲學之塔, C: 寶石洞窟 | |
| images: { A: 'https://i.meee.com.tw/rmcY2Vc.png', B: 'https://i.meee.com.tw/qV8QrUI.png', C: 'https://i.meee.com.tw/i67LLqV.png' }, | |
| correctAnswer: 'A', // 正確答案是代數之丘 | |
| feedback: { correct: "有了相對次數這項統計數據,是不是就更容易做選擇了呢~", wrong: "再仔細看看,哪個市場的需求比例最高呢?" } | |
| } | |
| ]; | |
| let firstChoice = null; | |
| const fishesOnScreen = []; | |
| // --- 畫面管理 --- | |
| function showScreen(screenId) { | |
| ['start-screen', 'game-screen', 'market-screen', 'loading-screen', 'strategy-screen', 'final-secret-screen'].forEach(id => { | |
| document.getElementById(id).style.display = (id === screenId) ? 'flex' : 'none'; | |
| }); | |
| } | |
| function showOverlay(overlayId) { | |
| overlayContainer.style.display = 'flex'; | |
| ['instructions-screen', 'game-over-screen', 'win-screen'].forEach(id => { | |
| document.getElementById(id).style.display = (id === overlayId) ? 'flex' : 'none'; | |
| }); | |
| } | |
| function hideOverlay() { | |
| overlayContainer.style.display = 'none'; | |
| } | |
| // --- 預載入函式 --- | |
| function preloadImages(urls, onProgress) { | |
| return new Promise((resolve) => { | |
| let loadedCount = 0; | |
| const totalImages = urls.length; | |
| if (totalImages === 0) resolve(); | |
| urls.forEach(url => { | |
| const img = new Image(); | |
| img.src = url; | |
| const onFinish = () => { | |
| loadedCount++; | |
| onProgress(loadedCount, totalImages); | |
| if (loadedCount === totalImages) resolve(); | |
| }; | |
| img.onload = onFinish; | |
| img.onerror = () => { console.error(`圖片載入失敗: ${url}`); onFinish(); }; | |
| }); | |
| }); | |
| } | |
| function startGame(levelIndex) { | |
| currentLevel = levelIndex; | |
| currentLevelSettings = levels[currentLevel]; | |
| score = 0; | |
| timeLeft = currentLevelSettings.time; | |
| updateUI(); | |
| fishesOnScreen.forEach(fish => fish.element.remove()); | |
| fishesOnScreen.length = 0; | |
| fishSpawnInterval = setInterval(spawnFish, currentLevelSettings.spawnRate); | |
| timerInterval = setInterval(() => { | |
| timeLeft--; | |
| updateUI(); | |
| if (timeLeft <= 0) endGame(false); | |
| }, 1000); | |
| gameInterval = setInterval(updateGame, 1000 / 60); | |
| } | |
| function spawnFish() { | |
| if (fishesOnScreen.length >= MAX_FISH_ON_SCREEN) return; | |
| let fishData; | |
| const fishSet = currentLevelSettings.fishSet.map(name => fishTypes.find(f => f.name === name)); | |
| const targetFish = fishSet.find(f => f.name === currentLevelSettings.targetFish); | |
| const otherFish = fishSet.filter(f => f.name !== currentLevelSettings.targetFish); | |
| const activeDistractor = otherFish.length > 0 ? otherFish[0] : targetFish; | |
| if (Math.random() < 0.75) { | |
| fishData = targetFish; | |
| } else { | |
| fishData = activeDistractor; | |
| } | |
| const fishElement = document.createElement('img'); | |
| fishElement.src = fishData.src; | |
| fishElement.className = 'fish'; | |
| const direction = Math.random() < 0.5 ? 'left' : 'right'; | |
| const speed = (Math.random() * 2 + 1.5) * currentLevelSettings.speedMultiplier; | |
| const startY = Math.random() * (gameContainer.clientHeight - fishData.height); | |
| fishElement.style.width = `${fishData.width}px`; | |
| fishElement.style.height = `${fishData.height}px`; | |
| fishElement.style.top = `${startY}px`; | |
| if (direction === 'left') { | |
| fishElement.style.left = `${gameContainer.clientWidth}px`; | |
| fishElement.style.transform = 'scaleX(-1)'; | |
| } else { | |
| fishElement.style.left = `-${fishData.width}px`; | |
| } | |
| const fishObject = { element: fishElement, data: fishData, speed, direction, vy: (Math.random() - 0.5) * 2, lastVyChange: Date.now() }; | |
| if (currentLevel === 2 && fishData.name === '水母') { | |
| fishObject.health = 2; | |
| } else { | |
| fishObject.health = 1; | |
| } | |
| fishElement.onclick = () => catchFish(fishObject); | |
| fishesOnScreen.push(fishObject); | |
| gameContainer.appendChild(fishObject.element); | |
| } | |
| function updateGame() { | |
| for (let i = fishesOnScreen.length - 1; i >= 0; i--) { | |
| const fish = fishesOnScreen[i]; | |
| let currentX = parseFloat(fish.element.style.left); | |
| let currentY = parseFloat(fish.element.style.top); | |
| if (fish.direction === 'left') { | |
| currentX -= fish.speed; | |
| if (currentX < -fish.data.width) { | |
| fish.element.remove(); | |
| fishesOnScreen.splice(i, 1); | |
| continue; | |
| } | |
| } else { | |
| currentX += fish.speed; | |
| if (currentX > gameContainer.clientWidth) { | |
| fish.element.remove(); | |
| fishesOnScreen.splice(i, 1); | |
| continue; | |
| } | |
| } | |
| fish.element.style.left = `${currentX}px`; | |
| if (currentLevel > 0) { | |
| if (Date.now() - fish.lastVyChange > 1000) { | |
| fish.vy = (Math.random() - 0.5) * 6; | |
| fish.lastVyChange = Date.now(); | |
| } | |
| currentY += fish.vy; | |
| if (currentY < 0 || currentY > gameContainer.clientHeight - fish.data.height) { | |
| fish.vy *= -1; | |
| } | |
| fish.element.style.top = `${currentY}px`; | |
| } | |
| } | |
| } | |
| function catchFish(fishObject) { | |
| fishObject.health--; | |
| if (fishObject.health > 0) { | |
| showCatchFeedback('!', fishObject.element, false); | |
| fishObject.element.style.opacity = '0.5'; | |
| setTimeout(() => { | |
| if (fishObject.element) { | |
| fishObject.element.style.opacity = '1'; | |
| } | |
| }, 150); | |
| return; | |
| } | |
| if (fishObject.data.name === currentLevelSettings.targetFish) { | |
| score++; | |
| showCatchFeedback('+1', fishObject.element, true); | |
| } else { | |
| score = Math.max(0, score - 1); | |
| showCatchFeedback('-1', fishObject.element, false); | |
| } | |
| fishObject.element.remove(); | |
| const index = fishesOnScreen.indexOf(fishObject); | |
| if (index > -1) fishesOnScreen.splice(index, 1); | |
| updateUI(); | |
| if (score >= currentLevelSettings.goal) endGame(true); | |
| } | |
| function showCatchFeedback(text, fishElement, isCorrect) { | |
| const feedback = document.createElement('div'); | |
| feedback.className = 'catch-feedback'; | |
| feedback.textContent = text; | |
| feedback.style.color = isCorrect ? '#4ade80' : '#f87171'; | |
| const rect = fishElement.getBoundingClientRect(); | |
| const containerRect = gameContainer.getBoundingClientRect(); | |
| feedback.style.left = `${rect.left - containerRect.left + rect.width / 2}px`; | |
| feedback.style.top = `${rect.top - containerRect.top + rect.height / 2}px`; | |
| gameContainer.appendChild(feedback); | |
| setTimeout(() => feedback.remove(), 1000); | |
| } | |
| function updateUI() { | |
| scoreDisplay.textContent = score; | |
| timerDisplay.textContent = timeLeft; | |
| } | |
| function endGame(isWin) { | |
| clearInterval(gameInterval); | |
| clearInterval(timerInterval); | |
| clearInterval(fishSpawnInterval); | |
| gameInterval = timerInterval = fishSpawnInterval = null; | |
| if (isWin) { | |
| showOverlay('win-screen'); | |
| } else { | |
| showOverlay('game-over-screen'); | |
| } | |
| } | |
| function setupMarketScreen() { | |
| const marketLevel = marketLevels[currentLevel]; | |
| document.querySelectorAll('.market-fish-name').forEach(el => el.textContent = levels[currentLevel].targetFish); | |
| marketImagesContainer.innerHTML = ''; | |
| const marketOrder = ['A', 'B', 'C']; | |
| marketOrder.forEach(key => { | |
| const src = marketLevel.images[key]; | |
| const choiceContainer = document.createElement('div'); | |
| choiceContainer.className = 'market-choice relative p-2 rounded-lg border-4 border-transparent cursor-pointer transition-all duration-200'; | |
| choiceContainer.dataset.market = key; | |
| const img = document.createElement('img'); | |
| img.src = src; | |
| img.alt = `[統計圖表]`; | |
| img.className = 'w-full h-auto rounded-lg shadow-lg'; | |
| const zoomIcon = document.createElement('button'); | |
| zoomIcon.className = 'absolute top-2 right-2 bg-black/50 p-2 rounded-full text-white hover:bg-black/80 transition-colors z-10'; | |
| zoomIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8zm8-3a1 1 0 011 1v2h2a1 1 0 110 2h-2v2a1 1 0 11-2 0v-2H5a1 1 0 110-2h2V6a1 1 0 011-1z" clip-rule="evenodd" /></svg>`; | |
| zoomIcon.onclick = (e) => { | |
| e.stopPropagation(); | |
| showEnlargedImage(src); | |
| }; | |
| choiceContainer.appendChild(img); | |
| choiceContainer.appendChild(zoomIcon); | |
| choiceContainer.addEventListener('click', handleMarketChoice); | |
| marketImagesContainer.appendChild(choiceContainer); | |
| }); | |
| marketFeedback.textContent = ''; | |
| nextLevelButton.classList.add('hidden'); | |
| firstChoice = null; | |
| } | |
| function handleMarketChoice(e) { | |
| const choiceContainer = e.target.closest('.market-choice'); | |
| if (!choiceContainer) return; | |
| const choice = choiceContainer.dataset.market; | |
| const marketLevel = marketLevels[currentLevel]; | |
| document.querySelectorAll('.market-choice').forEach(div => div.classList.remove('selected')); | |
| if (currentLevel === 0) { | |
| if (choice === marketLevel.correctAnswer) { | |
| choiceContainer.classList.add('selected'); | |
| marketFeedback.textContent = marketLevel.feedback.correct; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400'; | |
| nextLevelButton.textContent = "前往第二關"; | |
| nextLevelButton.classList.remove('hidden'); | |
| } else { | |
| marketFeedback.textContent = marketLevel.feedback.wrong; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-yellow-400'; | |
| } | |
| } else if (currentLevel === 1) { | |
| if (firstChoice === choice) { | |
| if (choice === marketLevel.correctAnswer) { | |
| choiceContainer.classList.add('selected'); | |
| marketFeedback.textContent = marketLevel.feedback.correct; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400'; | |
| nextLevelButton.textContent = "經營之神的攻略"; | |
| nextLevelButton.classList.remove('hidden'); | |
| } else { | |
| marketFeedback.textContent = marketLevel.feedback.wrong; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-red-500'; | |
| } | |
| firstChoice = null; | |
| } else { | |
| firstChoice = choice; | |
| choiceContainer.classList.add('selected'); | |
| marketFeedback.textContent = marketLevel.feedback.firstClick[choice]; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-cyan-300'; | |
| } | |
| } else if (currentLevel === 2) { | |
| if (choice === marketLevel.correctAnswer) { | |
| choiceContainer.classList.add('selected'); | |
| marketFeedback.textContent = marketLevel.feedback.correct; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-green-400'; | |
| nextLevelButton.textContent = "查看海風港灣的秘密"; | |
| nextLevelButton.classList.remove('hidden'); | |
| } else { | |
| marketFeedback.textContent = marketLevel.feedback.wrong; | |
| marketFeedback.className = 'text-center text-xl font-bold min-h-[32px] mt-4 text-red-500'; | |
| } | |
| } | |
| } | |
| function showEnlargedImage(src) { | |
| zoomedImage.src = src; | |
| zoomModal.classList.remove('hidden'); | |
| } | |
| function hideEnlargedImage() { | |
| zoomModal.classList.add('hidden'); | |
| zoomedImage.src = ''; | |
| } | |
| // --- 事件監聽 --- | |
| startButton.addEventListener('click', async () => { | |
| showScreen('loading-screen'); | |
| const imageUrls = [ | |
| ...fishTypes.map(fish => fish.src), | |
| ...Object.values(marketLevels[0].images), | |
| ...Object.values(marketLevels[1].images), | |
| ...Object.values(marketLevels[2].images), | |
| 'https://i.meee.com.tw/i67LLqV.png' | |
| ]; | |
| await preloadImages(imageUrls, (loaded, total) => { | |
| loadingText.textContent = `正在準備海底世界... (${loaded}/${total})`; | |
| }); | |
| showScreen('game-screen'); | |
| instructionsContent.innerHTML = ` | |
| <h2 class="text-3xl font-bold text-amber-300 mb-6">第一關任務</h2> | |
| <div class="space-y-4 text-lg"> | |
| <p>在 <span class="text-yellow-400 font-bold">${levels[0].time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levels[0].goal}</span> 隻${levels[0].targetFish}!</p> | |
| <p>點擊<span class="text-green-400 font-bold">正確的魚</span>來加分。</p> | |
| <p>注意!抓到<span class="text-red-400 font-bold">錯誤的魚</span>會扣分喔!</p> | |
| </div> | |
| <button id="play-game-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8"> | |
| 了解! | |
| </button>`; | |
| document.getElementById('play-game-button').onclick = () => { hideOverlay(); startGame(0); }; | |
| showOverlay('instructions-screen'); | |
| goalText.textContent = `抓 ${levels[0].goal} 隻${levels[0].targetFish}`; | |
| goalCount.textContent = levels[0].goal; | |
| }); | |
| restartButton.addEventListener('click', () => { | |
| hideOverlay(); | |
| startGame(currentLevel); | |
| }); | |
| continueButton.addEventListener('click', () => { | |
| hideOverlay(); | |
| showScreen('market-screen'); | |
| // FIX: Update the fish name immediately when showing the market screen instructions. | |
| document.querySelectorAll('.market-fish-name').forEach(el => el.textContent = levels[currentLevel].targetFish); | |
| marketInstructions.style.display = 'block'; | |
| marketChoices.style.display = 'none'; | |
| }); | |
| showMarketChoicesButton.addEventListener('click', () => { | |
| marketInstructions.style.display = 'none'; | |
| marketChoices.style.display = 'flex'; | |
| setupMarketScreen(); | |
| }); | |
| nextLevelButton.addEventListener('click', () => { | |
| if (currentLevel === 0) { | |
| const nextLevel = currentLevel + 1; | |
| const levelSettings = levels[nextLevel]; | |
| showScreen('game-screen'); | |
| instructionsContent.innerHTML = ` | |
| <h2 class="text-3xl font-bold text-amber-300 mb-6">第 ${nextLevel + 1} 關任務</h2> | |
| <div class="space-y-4 text-lg"> | |
| <p>在 <span class="text-yellow-400 font-bold">${levelSettings.time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levelSettings.goal}</span> 隻${levelSettings.targetFish}!</p> | |
| <p>海洋生物的移動方式不一樣了,小心!</p> | |
| </div> | |
| <button id="play-next-level-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8"> | |
| 挑戰! | |
| </button>`; | |
| document.getElementById('play-next-level-button').onclick = () => { hideOverlay(); startGame(nextLevel); }; | |
| showOverlay('instructions-screen'); | |
| goalText.textContent = `抓 ${levelSettings.goal} 隻${levelSettings.targetFish}`; | |
| goalCount.textContent = levelSettings.goal; | |
| } else if (currentLevel === 1) { | |
| showScreen('strategy-screen'); | |
| } else if (currentLevel === 2) { | |
| showScreen('final-secret-screen'); | |
| } | |
| }); | |
| startFinalStageButton.addEventListener('click', () => { | |
| const nextLevel = currentLevel + 1; | |
| const levelSettings = levels[nextLevel]; | |
| showScreen('game-screen'); | |
| instructionsContent.innerHTML = ` | |
| <h2 class="text-3xl font-bold text-amber-300 mb-6">最終挑戰!</h2> | |
| <div class="space-y-4 text-lg"> | |
| <p>在 <span class="text-yellow-400 font-bold">${levelSettings.time}</span> 秒內,捕捉 <span class="text-yellow-400 font-bold">${levelSettings.goal}</span> 隻${levelSettings.targetFish}!</p> | |
| <p class="text-cyan-300">注意!這次的${levelSettings.targetFish}比較頑強,需要點擊兩下才能捕捉!</p> | |
| </div> | |
| <button id="play-final-level-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-3 px-8 rounded-lg text-xl mt-8"> | |
| 挑戰! | |
| </button>`; | |
| document.getElementById('play-final-level-button').onclick = () => { hideOverlay(); startGame(nextLevel); }; | |
| showOverlay('instructions-screen'); | |
| goalText.textContent = `抓 ${levelSettings.goal} 隻${levelSettings.targetFish}`; | |
| goalCount.textContent = levelSettings.goal; | |
| }); | |
| zoomModal.addEventListener('click', () => { | |
| hideEnlargedImage(); | |
| }); | |
| // --- 初始啟動 --- | |
| showScreen('start-screen'); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |