802MathCity / index.html
Lashtw's picture
Upload 102 files
09cfa48 verified
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, 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>