Spaces:
Running
Running
| <html lang="zh-TW"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" | |
| content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <title>Math City: Cyber Chronicles</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;700;900&family=Orbitron:wght@400;700&display=swap" | |
| rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Noto Sans TC', sans-serif; | |
| background-color: #050510; | |
| overflow: hidden; | |
| color: white; | |
| /* Prevent bouncing on iOS */ | |
| overscroll-behavior: none; | |
| } | |
| .font-tech { | |
| font-family: 'Orbitron', sans-serif; | |
| } | |
| /* Map Container */ | |
| #map-container { | |
| position: relative; | |
| width: 100vw; | |
| height: 100vh; | |
| height: 100dvh; | |
| /* Fallback + Mobile fix */ | |
| background-image: url('Assets/index/indexbg.png'); | |
| background-size: cover; | |
| background-position: center; | |
| background-repeat: no-repeat; | |
| /* Ensure no overflow scrolling */ | |
| touch-action: none; | |
| } | |
| /* Effect Canvas */ | |
| #effect-canvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 15; | |
| /* Between background and pins */ | |
| } | |
| /* Pin Styles - Optimized for touch */ | |
| .map-pin { | |
| position: absolute; | |
| width: 200px; | |
| /* Larger touch area */ | |
| height: 120px; | |
| transform: translate(-50%, -50%); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| z-index: 20; | |
| /* Tap highlight removal */ | |
| -webkit-tap-highlight-color: transparent; | |
| } | |
| .pin-marker { | |
| width: 48px; | |
| /* Larger marker */ | |
| height: 48px; | |
| border-radius: 50%; | |
| border: 3px solid white; | |
| position: relative; | |
| box-shadow: 0 0 15px currentColor; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| animation: pulse 2s infinite; | |
| transition: transform 0.2s; | |
| } | |
| .pin-marker::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 20px; | |
| height: 20px; | |
| background-color: white; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| .pin-label { | |
| margin-top: 12px; | |
| background: rgba(15, 23, 42, 0.95); | |
| border: 1px solid currentColor; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| text-align: center; | |
| opacity: 1; | |
| /* Always visible on iPad */ | |
| backdrop-filter: blur(4px); | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s; | |
| } | |
| .map-pin:active .pin-marker { | |
| transform: scale(0.9); | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 20px rgba(255, 255, 255, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); | |
| } | |
| } | |
| /* Positions - Updated based on User Feedback | |
| Map Layout: | |
| Top-Left: Header Area | |
| Top-Right-ish: Skyscraper (摩天大樓 - Parallel) | |
| Center: Energy Core (Function) | |
| Bottom-Right: Congruence District | |
| Bottom-Left: Glitch Canyon (Sequence) | |
| */ | |
| #pin-parallel { | |
| top: 20%; | |
| left: 60%; | |
| color: #22c55e; | |
| /* Green */ | |
| } | |
| #pin-congruence { | |
| top: 75%; | |
| left: 75%; | |
| color: #d946ef; | |
| /* Magenta */ | |
| } | |
| #pin-sequence { | |
| top: 75%; | |
| left: 25%; | |
| color: #f59e0b; | |
| /* Amber/Orange */ | |
| } | |
| #pin-function { | |
| top: 50%; | |
| left: 50%; | |
| color: #06b6d4; | |
| /* Cyan for Energy */ | |
| } | |
| /* Scanlines - Subtle CRT effect */ | |
| .scanlines { | |
| background: linear-gradient(to bottom, | |
| rgba(255, 255, 255, 0), | |
| rgba(255, 255, 255, 0) 50%, | |
| rgba(0, 0, 0, 0.1) 50%, | |
| rgba(0, 0, 0, 0.1)); | |
| background-size: 100% 4px; | |
| position: fixed; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| left: 0; | |
| pointer-events: none; | |
| z-index: 50; | |
| } | |
| /* Cyber Header Style */ | |
| .cyber-header { | |
| background: rgba(5, 5, 16, 0.85); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border-left: 4px solid #06b6d4; | |
| border-bottom: 1px solid rgba(6, 182, 212, 0.3); | |
| padding: 20px 40px 20px 30px; | |
| /* Trapezoid shape effect via clip-path or just styling */ | |
| clip-path: polygon(0 0, 100% 0, 95% 100%, 0% 100%); | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); | |
| } | |
| /* Orientation Warning */ | |
| #orientation-warning { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: #000; | |
| z-index: 200; | |
| align-items: center; | |
| justify-content: center; | |
| text-align: center; | |
| padding: 20px; | |
| } | |
| @media screen and (orientation: portrait) { | |
| #orientation-warning { | |
| display: flex; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-black"> | |
| <!-- Orientation Warning --> | |
| <div id="orientation-warning"> | |
| <div class="text-center"> | |
| <div class="text-6xl mb-4">📱➡️📱</div> | |
| <h2 class="text-2xl font-bold text-white mb-2">請旋轉裝置</h2> | |
| <p class="text-slate-400">為了獲得最佳體驗,請使用橫向模式遊玩</p> | |
| </div> | |
| </div> | |
| <!-- Scanlines --> | |
| <div class="scanlines"></div> | |
| <!-- Main Map --> | |
| <div id="map-container"> | |
| <canvas id="effect-canvas"></canvas> | |
| <!-- Header (Moved to Top-Left with Cyber Mask) --> | |
| <div class="absolute top-8 left-0 z-20 pointer-events-none"> | |
| <div class="cyber-header"> | |
| <h1 | |
| class="text-4xl md:text-5xl font-black font-tech text-white drop-shadow-[0_0_5px_rgba(6,182,212,0.8)] tracking-wider"> | |
| MATH CITY | |
| </h1> | |
| <div class="flex items-center gap-2 mt-1"> | |
| <div class="h-1 w-8 bg-cyan-500"></div> | |
| <span class="text-xs font-mono text-cyan-300 tracking-[0.2em]">CYBER CHRONICLES</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Pin 1: Sequence Canyon (Bottom-Left) --> | |
| <div id="pin-sequence" class="map-pin group" onclick="triggerBeamNavigation(this, 'sequence.html')"> | |
| <div class="pin-marker border-amber-500 group-hover:scale-110"></div> | |
| <div class="pin-label border-amber-500 text-amber-400"> | |
| <div class="font-bold text-xl">數列峽谷</div> | |
| <div class="text-xs text-white opacity-80">The Glitch Canyon</div> | |
| </div> | |
| </div> | |
| <!-- Pin 2: Energy Core (Center) --> | |
| <div id="pin-function" class="map-pin group" onclick="triggerBeamNavigation(this, 'function.html')"> | |
| <div class="pin-marker group-hover:scale-110"></div> | |
| <div class="pin-label border-cyan-500 text-cyan-400"> | |
| <div class="font-bold text-xl">能源核心</div> | |
| <div class="text-xs text-white opacity-80">The Energy Core</div> | |
| </div> | |
| </div> | |
| <!-- Pin 3: Congruence District (Bottom-Right) --> | |
| <div id="pin-congruence" class="map-pin group" | |
| onclick="triggerBeamNavigation(this, 'congruence_detective.html')"> | |
| <div class="pin-marker border-fuchsia-500 group-hover:scale-110"></div> | |
| <div class="pin-label border-fuchsia-500 text-fuchsia-400"> | |
| <div class="font-bold text-xl">全等重案組</div> | |
| <div class="text-xs text-white opacity-80">Congruence District</div> | |
| </div> | |
| </div> | |
| <!-- Pin 4: Skyscraper (Top-Right-ish) --> | |
| <div id="pin-parallel" class="map-pin group" onclick="window.location.href='skyscraper.html'"> | |
| <div class="pin-marker border-green-500 group-hover:scale-110"></div> | |
| <div class="pin-label border-green-500 text-green-400"> | |
| <div class="font-bold text-xl">鋼鐵輸送帶</div> | |
| <div class="text-xs text-white opacity-80">Steel Conveyor</div> | |
| </div> | |
| </div> | |
| <!-- System Info Footer --> | |
| <div class="absolute bottom-4 left-4 right-4 flex justify-between items-end pointer-events-none opacity-60"> | |
| <div class="text-[10px] font-mono text-cyan-300"> | |
| SYS.ORD: 7749-X<br> | |
| SEC.LVL: ALPHA | |
| </div> | |
| </div> | |
| <!-- Credits Footer --> | |
| <div class="fixed bottom-1 right-2 text-right text-[10px] text-slate-500/50 pointer-events-none z-50 font-sans"> | |
| <div>遊戲設計:新竹縣精華國中 藍星宇老師</div> | |
| <div>臉書社團:<a href="https://www.facebook.com/groups/1554372228718393" target="_blank" | |
| class="hover:text-amber-400 pointer-events-auto transition-colors">萬物皆數</a></div> | |
| </div> | |
| <!-- Reset Button --> | |
| <button onclick="window.resetScores()" | |
| class="fixed bottom-4 left-4 z-50 bg-slate-900/80 text-xs text-slate-400 hover:text-white px-3 py-2 rounded border border-slate-700 hover:bg-red-900/50 transition-colors pointer-events-auto backdrop-blur-sm shadow-lg"> | |
| ↺ 重置分數 Reset | |
| </button> | |
| <!-- Replay Ending Button --> | |
| <button onclick="startEndGameSequence()" id="replay-ending-btn" | |
| class="hidden fixed bottom-16 left-4 z-50 bg-amber-600/80 text-white font-bold px-4 py-2 rounded-full border border-amber-400 hover:bg-amber-500 transition-colors pointer-events-auto shadow-[0_0_15px_rgba(251,191,36,0.5)]"> | |
| 🎬 重播結局 | |
| </button> | |
| </div> | |
| <!-- End Game Sequence --> | |
| <div id="endgame-layer" | |
| class="hidden fixed inset-0 z-[300] bg-black/95 backdrop-blur-md flex flex-col items-center justify-center p-4"> | |
| <!-- Dialog Phase --> | |
| <div id="endgame-dialog" class="absolute inset-0 flex items-center justify-center hidden cursor-pointer"> | |
| <div | |
| class="glass-panel p-8 md:p-12 rounded-2xl flex flex-col md:flex-row gap-8 max-w-5xl w-full items-center border border-amber-500/30 shadow-[0_0_50px_rgba(251,191,36,0.15)] relative overflow-hidden bg-slate-900/80"> | |
| <!-- Mayor Image --> | |
| <div | |
| class="relative w-48 h-48 md:w-64 md:h-64 flex-shrink-0 bg-slate-800 rounded-full border-[4px] border-amber-500/30 flex items-center justify-center p-4"> | |
| <img src="Assets/triangle/mayor.svg" alt="Mayor" | |
| class="w-[120%] h-[120%] object-contain filter drop-shadow-[0_0_15px_rgba(251,191,36,0.6)]"> | |
| </div> | |
| <!-- Text Box --> | |
| <div class="flex-1 flex flex-col gap-4 z-10 w-full"> | |
| <div | |
| class="font-tech text-amber-500 text-xl tracking-widest border-b border-amber-500/30 pb-2 flex justify-between"> | |
| <span>CITY MAYOR // 未來都市市長</span><span | |
| class="text-xs text-amber-500/50 animate-pulse hidden md:block">SECURE CONNECTION</span> | |
| </div> | |
| <div id="endgame-text" | |
| class="text-slate-200 text-xl md:text-2xl leading-relaxed min-h-[140px] font-bold"></div> | |
| <div id="endgame-continue" | |
| class="text-amber-400 text-sm animate-pulse font-tech mt-4 hidden text-right pointer-events-none"> | |
| CLICK TO CONTINUE ></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Name Input Phase --> | |
| <div id="endgame-name-phase" | |
| class="absolute inset-0 flex flex-col items-center justify-center hidden bg-black/90 z-[350]"> | |
| <div | |
| class="glass-panel p-8 md:p-12 rounded-2xl flex flex-col gap-6 max-w-xl w-[90%] md:w-full items-center border border-cyan-500/30 shadow-[0_0_50px_rgba(6,182,212,0.15)] bg-slate-900/95 text-center relative pointer-events-auto"> | |
| <h3 class="text-4xl font-black text-amber-400 mb-2 drop-shadow-[0_0_10px_rgba(251,191,36,0.5)]">登錄榮耀榜 | |
| </h3> | |
| <p class="text-slate-300 text-xl leading-relaxed mb-4"> | |
| 請輸入要刻在徽章上的名字,以便領取並證明你的榮耀! | |
| </p> | |
| <input type="text" id="badge-name-input" | |
| class="w-full bg-slate-800 text-white text-center rounded-xl border-2 border-slate-600 focus:border-amber-400 focus:ring-2 focus:ring-amber-400/50 outline-none p-4 text-2xl" | |
| placeholder="請輸入姓名..." maxlength="12"> | |
| <button id="confirm-name-btn" | |
| class="w-full py-4 mt-2 bg-gradient-to-r from-amber-600 to-amber-500 hover:from-amber-500 hover:to-amber-400 text-white font-bold rounded-xl text-2xl shadow-[0_0_20px_rgba(251,191,36,0.3)] transition-all transform hover:scale-105"> | |
| 鑄造榮耀徽章 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Badge Phase --> | |
| <div id="endgame-badge-phase" | |
| class="absolute inset-0 flex flex-col items-center justify-center hidden bg-black/90 cursor-pointer"> | |
| <div | |
| class="font-tech text-fuchsia-500 text-2xl tracking-[0.5em] mb-4 uppercase drop-shadow-[0_0_10px_rgba(217,70,239,0.8)]"> | |
| MISSION ACCOMPLISHED</div> | |
| <h2 class="text-5xl md:text-6xl font-black text-white mb-12 drop-shadow-md text-center">榮耀徽章</h2> | |
| <div id="badge-display" | |
| class="transform scale-0 opacity-0 transition-all duration-1000 ease-out flex flex-col items-center gap-6"> | |
| <!-- Badge SVG will be inserted here --> | |
| </div> | |
| <div id="badge-continue" | |
| class="text-white text-xl animate-bounce mt-16 text-slate-400 font-tech pointer-events-none">▼ CLICK TO | |
| VIEW CREDITS ▼</div> | |
| </div> | |
| <!-- Credits Phase --> | |
| <div id="endgame-credits" class="absolute inset-0 bg-black hidden overflow-hidden font-sans"> | |
| <div id="credits-scroll" class="absolute w-full px-4 flex flex-col items-center text-center pb-32" | |
| style="top: 100%; transition: transform 30s linear;"> | |
| <div class="mb-24"> | |
| <h2 | |
| class="text-5xl md:text-7xl font-black text-white font-tech tracking-widest drop-shadow-[0_0_20px_rgba(6,182,212,0.8)]"> | |
| MATH CITY</h2> | |
| <div class="text-cyan-400 font-mono tracking-widest mt-2 text-xl">CYBER CHRONICLES</div> | |
| </div> | |
| <div id="credits-content" class="flex flex-col text-slate-300 w-full max-w-3xl px-4 text-center"> | |
| <div class="mb-12 text-2xl md:text-3xl font-bold text-amber-400">遊戲設計:精華國中 藍星宇</div> | |
| <div class="mb-12 text-2xl md:text-3xl font-bold text-amber-400">美術設計:精華國中 藍星宇</div> | |
| <div class="mb-32 text-2xl md:text-3xl font-bold text-amber-400">劇情設計:精華國中 藍星宇</div> | |
| <div | |
| class="mb-48 text-4xl md:text-6xl font-black text-fuchsia-400 flex flex-col gap-6 items-center"> | |
| <span class="animate-bounce">...通通都是我啦 😆</span> | |
| </div> | |
| <div class="mb-16 text-slate-400 text-2xl">但只有我是遠遠不夠的...</div> | |
| <div class="mb-12 font-bold text-white text-3xl md:text-4xl drop-shadow-[0_0_10px_white]"> | |
| 感謝以下老師協助遊戲測試</div> | |
| <div | |
| class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-10 text-cyan-300 text-2xl font-bold mb-48 mx-auto w-full max-w-2xl px-8"> | |
| <div>八斗高中 郭心欣老師</div> | |
| <div>中華國中 褚煜凱老師</div> | |
| <div>林口國中 李政憲老師</div> | |
| <div>臉友 巫秀建老師</div> | |
| </div> | |
| <div class="mb-48 text-slate-400 text-xl text-center border-t border-b border-slate-800 py-12"> | |
| 使用AI工具:<br><span | |
| class="text-white font-tech tracking-wider text-3xl mt-4 block text-amber-300">Google | |
| Antigravity</span> | |
| </div> | |
| <div | |
| class="mb-32 text-xl md:text-2xl text-slate-300 leading-[2.5] max-w-3xl block mx-auto text-left relative z-10 px-8"> | |
| <p class="mb-6 indent-8">遊戲的設計相當複雜,難免會有很多 bug,若是有在遊戲過程中造成不好的體驗,星宇在這邊跟大家說抱歉了!</p> | |
| <p class="mb-12 indent-8">我也還在努力學習中,希望未來能做得越來越好,帶給大家更多的好玩的數學探索遊戲...</p> | |
| <p | |
| class="text-center text-amber-400 font-bold text-4xl mt-16 tracking-widest bg-gradient-to-r from-amber-400 to-yellow-600 bg-clip-text text-transparent transform hover:scale-105 transition-transform cursor-default"> | |
| 我們九年級上學期見了!</p> | |
| </div> | |
| <div class="font-bold text-3xl text-white tracking-[0.2em] opacity-80 mt-24">2026.2.23 星宇敬上</div> | |
| </div> | |
| </div> | |
| <div class="absolute bottom-24 opacity-0 transition-opacity duration-1000 flex justify-center w-full z-[400] pointer-events-none" | |
| id="credits-end-button"> | |
| <button onclick="closeEndGame()" | |
| class="pointer-events-auto bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-bold py-6 px-16 rounded-full text-3xl shadow-[0_0_40px_rgba(79,70,229,0.5)] transition-all transform hover:scale-110 cursor-pointer"> | |
| 回到 Math City > | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Prevent default touch actions (zooming, scrolling) | |
| document.body.addEventListener('touchmove', function (e) { | |
| e.preventDefault(); | |
| }, { passive: false }); | |
| // Effect Canvas System | |
| const canvas = document.getElementById('effect-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| let width, height; | |
| function resize() { | |
| width = canvas.width = window.innerWidth; | |
| height = canvas.height = window.innerHeight; | |
| } | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| // Load High Score | |
| // Load High Scores | |
| // Load High Scores | |
| function loadScores() { | |
| // Helper to update pin | |
| const updatePin = (pinId, scoreKey) => { | |
| const score = localStorage.getItem(scoreKey); | |
| if (score) { | |
| const pin = document.querySelector(pinId); | |
| if (!pin) return; | |
| // Fix: Ensure pin marker doesn't squash when content grows | |
| const marker = pin.querySelector('.pin-marker'); | |
| if (marker) marker.style.flexShrink = '0'; | |
| // Update Label | |
| const label = pin.querySelector('.pin-label'); | |
| if (label) { | |
| // 1. Add Trophy to Text if not present | |
| if (!label.innerText.includes('🏆')) { | |
| // We need to be careful not to overwrite if we run this multiple times | |
| // Get direct text content ignoring children | |
| const currentText = label.childNodes[0].textContent.trim(); | |
| label.firstChild.textContent = `🏆 ${currentText}`; | |
| } | |
| // 2. Add Score Badge BELOW the text | |
| // Check if score div exists to prevent duplicates | |
| let scoreDiv = label.querySelector('.score-badge'); | |
| if (!scoreDiv) { | |
| scoreDiv = document.createElement('div'); | |
| scoreDiv.className = 'score-badge text-[10px] text-amber-300 font-bold mt-1 tracking-wider border-t border-slate-600 pt-1'; | |
| label.appendChild(scoreDiv); | |
| } | |
| scoreDiv.innerText = `BEST: ${score}`; | |
| // Ensure label handles the content vertical flow correctly | |
| label.style.display = 'flex'; | |
| label.style.flexDirection = 'column'; | |
| label.style.alignItems = 'center'; | |
| label.style.gap = '2px'; | |
| } | |
| } | |
| }; | |
| updatePin('#pin-function', 'math_city_score_function'); | |
| updatePin('#pin-sequence', 'math_city_score_sequence'); | |
| updatePin('#pin-congruence', 'math_city_score_congruence'); | |
| updatePin('#pin-parallel', 'math_city_score_parallel'); | |
| } | |
| loadScores(); | |
| // Global Reset Function | |
| window.resetScores = function () { | |
| if (confirm('確定要重置所有分數與獎盃嗎?\nAre you sure you want to reset all scores?')) { | |
| localStorage.removeItem('math_city_score_function'); | |
| localStorage.removeItem('math_city_score_sequence'); | |
| localStorage.removeItem('math_city_score_congruence'); | |
| localStorage.removeItem('math_city_score_congruence_val'); | |
| localStorage.removeItem('math_city_score_parallel'); | |
| localStorage.removeItem('math_city_ending_seen'); | |
| location.reload(); | |
| } | |
| }; | |
| // --- End Game Sequence Logic --- | |
| function getBadgeInfo(score) { | |
| // New balanced thresholds for ~8000 max score | |
| if (score >= 7500) return { grade: 'S', color: '#fbbf24' }; | |
| if (score >= 6500) return { grade: 'A++', color: '#d946ef' }; | |
| if (score >= 5000) return { grade: 'A+', color: '#a855f7' }; | |
| if (score >= 3500) return { grade: 'A', color: '#22d3ee' }; | |
| return { grade: 'B', color: '#4ade80' }; | |
| } | |
| function calculateTotalScore() { | |
| const s1 = parseInt(localStorage.getItem('math_city_score_sequence') || 0); | |
| const s2 = parseInt(localStorage.getItem('math_city_score_function') || 0); | |
| const s3 = parseInt(localStorage.getItem('math_city_score_parallel') || 0); | |
| let val4 = localStorage.getItem('math_city_score_congruence_val'); | |
| if (!val4) { | |
| const grade4 = localStorage.getItem('math_city_score_congruence'); | |
| if (!grade4) return 0; // Not finished | |
| const mapper = { 'S': 95, 'A++': 85, 'A+': 75, 'A': 65, 'B': 50, 'C': 30 }; | |
| val4 = mapper[grade4] || 30; | |
| } | |
| // Bonus logic for Sequence hidden stage | |
| const seqBonus = localStorage.getItem('sequence_negative_passed') === 'true' ? 1000 : 0; | |
| const hiddenSeqScore = parseInt(localStorage.getItem('math_city_hidden_score_sequence') || 0); | |
| const hasExtraBonus = seqBonus > 0 || hiddenSeqScore > 0; | |
| const extraBonus = hasExtraBonus ? 500 : 0; | |
| // Updated Weights (Targeting ~2000 points per game) | |
| // s1 (~1000) -> * 2 | |
| // s2 (~6350) -> / 3 | |
| // s3 (~9 stages) -> * 200 | |
| // s4 (~100) -> * 20 | |
| const finalS1 = s1 * 2; | |
| const finalS2 = s2 / 3; | |
| const finalS3 = s3 * 200; | |
| const finalS4 = parseFloat(val4) * 20; | |
| const total = finalS1 + extraBonus + finalS2 + finalS3 + finalS4; | |
| return total; | |
| } | |
| function generateBadgeSVG(grade, color, name = '') { | |
| return ` | |
| <svg width="240" height="280" viewBox="0 0 240 280" style="filter: drop-shadow(0 0 20px ${color})"> | |
| <!-- Base Shield --> | |
| <path d="M120 10 L220 50 L220 150 C220 220 120 270 120 270 C120 270 20 220 20 150 L20 50 Z" fill="rgba(15,23,42,0.9)" stroke="${color}" stroke-width="6" /> | |
| <!-- Inner Border --> | |
| <path d="M120 25 L200 60 L200 145 C200 200 120 245 120 245 C120 245 40 200 40 145 L40 60 Z" fill="none" stroke="${color}" stroke-width="2" stroke-dasharray="6,4" opacity="0.6"/> | |
| <!-- Glow backing for letter --> | |
| <circle cx="120" cy="115" r="50" fill="${color}" opacity="0.1" /> | |
| <circle cx="120" cy="115" r="60" fill="none" stroke="${color}" stroke-width="1" opacity="0.3" /> | |
| <!-- Grade Text --> | |
| <text x="120" y="145" font-family="'Orbitron', sans-serif" font-weight="900" font-size="80" fill="${color}" text-anchor="middle" style="text-shadow: 0 0 15px ${color}"> | |
| ${grade} | |
| </text> | |
| <!-- Name Text (Embossed Effect) --> | |
| <text x="122" y="200" font-family="'Noto Sans TC', sans-serif" font-weight="900" font-size="22" fill="${color}" text-anchor="middle" style="letter-spacing: 4px; text-shadow: -1px -1px 0 rgba(255,255,255,0.4), 1px 1px 3px rgba(0,0,0,0.9), 2px 2px 5px rgba(0,0,0,0.6);"> | |
| ${name} | |
| </text> | |
| </svg> | |
| `; | |
| } | |
| function checkEndGame() { | |
| const hasCompletedAll = ( | |
| localStorage.getItem('math_city_score_sequence') && | |
| localStorage.getItem('math_city_score_function') && | |
| localStorage.getItem('math_city_score_parallel') && | |
| localStorage.getItem('math_city_score_congruence') | |
| ); | |
| const replayBtn = document.getElementById('replay-ending-btn'); | |
| if (hasCompletedAll) { | |
| if (replayBtn) replayBtn.classList.remove('hidden'); | |
| const hasSeen = localStorage.getItem('math_city_ending_seen') === 'true'; | |
| if (!hasSeen) { | |
| const totalScore = calculateTotalScore(); | |
| // auto trigger ending 1.5s after loading index | |
| setTimeout(() => startEndGameSequence(totalScore), 1500); | |
| } | |
| } | |
| } | |
| let audioMayor = new Audio('Assets/1.mp3'); | |
| let audioCredits = new Audio('Assets/2.mp3'); | |
| let isEndgameTyping = false; | |
| function startEndGameSequence(score = null) { | |
| // Stop and replay mayor music | |
| audioCredits.pause(); | |
| audioCredits.currentTime = 0; | |
| audioMayor.currentTime = 0; | |
| audioMayor.loop = true; | |
| audioMayor.play().catch(e => console.log("Audio play prevented:", e)); | |
| // Close pin actions or hover states if any | |
| if (score === null) { | |
| score = calculateTotalScore(); | |
| } | |
| const badgeInfo = getBadgeInfo(score); | |
| let nickname = localStorage.getItem('player_nickname') || '未知的數學家'; | |
| const layer = document.getElementById('endgame-layer'); | |
| const dialogPhase = document.getElementById('endgame-dialog'); | |
| const textEl = document.getElementById('endgame-text'); | |
| const continueBtn = document.getElementById('endgame-continue'); | |
| layer.classList.remove('hidden'); | |
| dialogPhase.classList.remove('hidden'); | |
| document.getElementById('endgame-badge-phase').classList.add('hidden'); | |
| document.getElementById('endgame-credits').classList.add('hidden'); | |
| const dialogLines = [ | |
| `是<span class="text-amber-400 font-bold ml-1 mr-1 text-2xl md:text-3xl">${nickname}</span>阿!聽說你在短短時間內,不僅通過了危險的數列峽谷,還協助警方抓到三個犯人...`, | |
| `更成功的解決了城市的電力危機,還幫我們修復了鋼鐵輸送帶!`, | |
| `在這個未來都市中,很久沒有看到這麼有前途的年輕人了!希望未來你能一切順利,在學習數學的路上走得又穩又遠。`, | |
| `這個<span class="text-fuchsia-400 font-bold ml-1 mr-1">榮耀徽章</span>,就當作我給你的祝福了!` | |
| ]; | |
| let currentLine = 0; | |
| document.getElementById('badge-display').className = "transform scale-0 opacity-0 transition-all duration-1000 ease-out flex flex-col items-center gap-6"; | |
| document.getElementById('badge-continue').classList.add('hidden'); | |
| function showLine() { | |
| if (currentLine >= dialogLines.length) { | |
| promptForName(badgeInfo); | |
| return; | |
| } | |
| textEl.innerHTML = dialogLines[currentLine]; | |
| textEl.style.opacity = '0'; | |
| textEl.style.transform = 'translateY(10px)'; | |
| textEl.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; | |
| continueBtn.classList.add('hidden'); | |
| isEndgameTyping = true; | |
| setTimeout(() => { | |
| textEl.style.opacity = '1'; | |
| textEl.style.transform = 'translateY(0)'; | |
| setTimeout(() => { | |
| isEndgameTyping = false; | |
| continueBtn.classList.remove('hidden'); | |
| }, 500); | |
| }, 50); | |
| currentLine++; | |
| } | |
| dialogPhase.onclick = () => { | |
| if (!isEndgameTyping) showLine(); | |
| }; | |
| textEl.innerHTML = ''; | |
| setTimeout(showLine, 800); | |
| } | |
| function promptForName(badgeInfo) { | |
| document.getElementById('endgame-dialog').classList.add('hidden'); | |
| const namePhase = document.getElementById('endgame-name-phase'); | |
| namePhase.classList.remove('hidden'); | |
| const inputElement = document.getElementById('badge-name-input'); | |
| const confirmBtn = document.getElementById('confirm-name-btn'); | |
| // Prefill if available | |
| inputElement.value = localStorage.getItem('player_nickname') || ''; | |
| inputElement.focus(); | |
| // Bind click and enter key to confirm | |
| const handleConfirm = () => { | |
| const name = inputElement.value.trim() || '未知的數學家'; | |
| localStorage.setItem('player_nickname', name); | |
| // Generate and inject SVG | |
| document.getElementById('badge-display').innerHTML = generateBadgeSVG(badgeInfo.grade, badgeInfo.color, name); | |
| namePhase.classList.add('hidden'); | |
| showBadgePhase(); | |
| }; | |
| confirmBtn.onclick = handleConfirm; | |
| inputElement.onkeydown = (e) => { | |
| if (e.key === 'Enter') handleConfirm(); | |
| }; | |
| } | |
| function showBadgePhase() { | |
| document.getElementById('endgame-dialog').classList.add('hidden'); | |
| const badgePhase = document.getElementById('endgame-badge-phase'); | |
| badgePhase.classList.remove('hidden'); | |
| setTimeout(() => { | |
| const badgeDisplay = document.getElementById('badge-display'); | |
| badgeDisplay.classList.remove('scale-0', 'opacity-0'); | |
| badgeDisplay.classList.add('scale-100', 'opacity-100'); | |
| setTimeout(() => { | |
| document.getElementById('badge-continue').classList.remove('hidden'); | |
| badgePhase.onclick = () => showCreditsPhase(); | |
| }, 1500); | |
| }, 300); | |
| } | |
| function showCreditsPhase() { | |
| // Stop Mayor music and play Credits music | |
| audioMayor.pause(); | |
| audioCredits.currentTime = 0; | |
| audioCredits.play().catch(e => console.log("Audio play prevented:", e)); | |
| document.getElementById('endgame-badge-phase').classList.add('hidden'); | |
| const creditsPhase = document.getElementById('endgame-credits'); | |
| creditsPhase.classList.remove('hidden'); | |
| const scrollEl = document.getElementById('credits-scroll'); | |
| // reset transform (top:100% already puts it at the bottom border, 5vh gives a tiny breath) | |
| scrollEl.style.transform = 'translateY(5vh)'; | |
| scrollEl.style.transition = 'none'; | |
| // force reflow | |
| void scrollEl.offsetWidth; | |
| // Apply animation | |
| // Using 30s for credits scroll | |
| scrollEl.style.transition = 'transform 30s linear'; | |
| scrollEl.style.transform = 'translateY(-120%)'; | |
| setTimeout(() => { | |
| document.getElementById('credits-end-button').classList.remove('opacity-0'); | |
| localStorage.setItem('math_city_ending_seen', 'true'); | |
| document.getElementById('replay-ending-btn').classList.remove('hidden'); | |
| }, 30000); | |
| } | |
| function closeEndGame() { | |
| audioCredits.pause(); | |
| document.getElementById('endgame-layer').classList.add('hidden'); | |
| localStorage.setItem('math_city_ending_seen', 'true'); | |
| document.getElementById('replay-ending-btn').classList.remove('hidden'); | |
| } | |
| // Check Endgame condition on load | |
| setTimeout(checkEndGame, 1000); | |
| function getPinColor(element) { | |
| // Extract color from computed style of the label text | |
| const label = element.querySelector('.pin-label'); | |
| return window.getComputedStyle(label).color; | |
| } | |
| function triggerBeamNavigation(element, url) { | |
| const rect = element.getBoundingClientRect(); | |
| const targetX = rect.left + rect.width / 2; | |
| const targetY = rect.top + rect.height / 2; | |
| const startX = width / 2; | |
| const startY = height / 2; | |
| const color = getPinColor(element); | |
| animateBeam(startX, startY, targetX, targetY, color, () => { | |
| window.location.href = url; | |
| }); | |
| } | |
| function animateBeam(x1, y1, x2, y2, color, onComplete) { | |
| const startTime = performance.now(); | |
| const duration = 600; // ms | |
| function loop(now) { | |
| const elapsed = now - startTime; | |
| const progress = Math.min(elapsed / duration, 1); | |
| // Ease In Out Quart | |
| const ease = progress < 0.5 ? 8 * progress * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 4) / 2; | |
| ctx.clearRect(0, 0, width, height); | |
| // Draw Beam | |
| const currentX = x1 + (x2 - x1) * ease; | |
| const currentY = y1 + (y2 - y1) * ease; | |
| // Trail | |
| ctx.beginPath(); | |
| ctx.moveTo(x1, y1); | |
| ctx.lineTo(currentX, currentY); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 4 + Math.sin(now * 0.02) * 2; | |
| ctx.lineCap = 'round'; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = color; | |
| ctx.globalAlpha = 1 - ease * 0.5; // Fade tail slightly | |
| ctx.stroke(); | |
| // Head Particle | |
| ctx.beginPath(); | |
| ctx.arc(currentX, currentY, 8, 0, Math.PI * 2); | |
| ctx.fillStyle = '#fff'; | |
| ctx.shadowBlur = 30; | |
| ctx.shadowColor = color; | |
| ctx.globalAlpha = 1; | |
| ctx.fill(); | |
| // Impact Ripple at target when close | |
| if (progress > 0.8) { | |
| const rippleSize = (progress - 0.8) * 1000; // Expand rapidly | |
| ctx.beginPath(); | |
| ctx.arc(x2, y2, rippleSize, 0, Math.PI * 2); | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 5 * (1 - (progress - 0.8) * 5); | |
| ctx.stroke(); | |
| } | |
| ctx.shadowBlur = 0; | |
| if (progress < 1) { | |
| requestAnimationFrame(loop); | |
| } else { | |
| onComplete(); | |
| } | |
| } | |
| requestAnimationFrame(loop); | |
| } | |
| </script> | |
| </body> | |
| </html> |