Spaces:
Running
Running
File size: 39,037 Bytes
392f67b af802e1 392f67b dbd41db 392f67b dbd41db 392f67b dbd41db 392f67b 09cfa48 392f67b 09cfa48 392f67b 09cfa48 392f67b dbd41db 392f67b dbd41db 09cfa48 dbd41db 09cfa48 392f67b 09cfa48 392f67b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 | <!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> |