套件原始碼 · 數學小森林
CC BY 4.0 · 請標明 @SpringFairy · v1
AI 編輯提示起頭
Edit this classroom game for interactive whiteboards. Keep large touch targets and whole-class play. Tracking, logins, personal-data collection, and remote API calls are prohibited. Use the downloaded complete ZIP—not only the entry HTML shown below—and preserve its browser-ready folder structure. Preserve the educational goal. Original creator: @SpringFairy on ClassBoard Games (CC BY 4.0).
僅入口 HTML · index.html · 275.2 KB
<!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">
<title>卡比夢幻森林大冒險 - 10以內加減法闖關遊戲 (Switch 卡比音效版)</title>
<style>
:root {
--pastel-sky: #bfe8ff;
--pastel-green: #e2f7d8;
--card-bg: rgba(255, 255, 255, 0.95);
--text-main: #2d3748;
--text-muted: #5a6b82;
--kirby-pink: #ff85a2;
--accent-gold: #ffb703;
--btn-shadow: rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
min-height: 100vh;
font-family: 'M PLUS Rounded 1c', 'Nunito', 'PingFang TC', 'Microsoft JhengHei', '微軟正黑體', sans-serif;
background: linear-gradient(180deg, #bfe8ff 0%, #d8f3dc 45%, #b7e4c7 100%);
color: var(--text-main);
overflow-x: hidden;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
/* 頂部輕量 HUD 狀態條 */
.top-hud {
width: 100%;
max-width: 980px;
padding: 10px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
z-index: 100;
position: relative;
}
.hud-left {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.hud-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(6px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 2px solid white;
font-size: 1.05rem;
font-weight: 800;
transition: transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.hud-badge.bounce {
transform: scale(1.22);
}
.badge-coins { color: #b45309; }
.badge-streak { color: #dc2626; }
.badge-stage { color: #15803d; }
.hud-right {
display: flex;
align-items: center;
gap: 8px;
}
.icon-btn {
width: 42px;
height: 42px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.92);
border: 2px solid white;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.2s ease;
}
.icon-btn:hover {
transform: scale(1.1);
background: #ffffff;
}
.icon-btn:active {
transform: scale(0.95);
}
.kirby-voice-btn {
background: linear-gradient(135deg, #fff0f5, #ffd6e0);
border-color: #ffb4cf;
color: #e11d48;
width: auto;
padding: 0 14px;
border-radius: 20px;
font-size: 0.95rem;
font-weight: 800;
gap: 4px;
}
/* 主遊戲舞台容器 */
.game-viewport {
width: 100%;
max-width: 980px;
padding: 0 12px 30px;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
z-index: 10;
}
/* =========================================================
🌲 沉浸式森林探險場景 (Forest Adventure Stage)
========================================================= */
.forest-stage {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
max-height: 520px;
border-radius: 28px;
overflow: hidden;
box-shadow: 0 16px 36px rgba(34, 84, 61, 0.2), 0 0 0 5px #ffffff;
background-color: #c7ebff;
background-image: url('forest_bg.jpg');
background-size: cover;
background-position: center bottom;
background-repeat: no-repeat;
}
/* 探索小徑上的 10 個發光踏石站點 (Checkpoints) */
.checkpoint-node {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 5;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.node-stone {
width: 38px;
height: 38px;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #ffffff 0%, #fff3b0 50%, #fcd34d 100%);
border: 3px solid #ffffff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25), 0 0 14px rgba(254, 240, 138, 0.8);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
font-weight: 900;
color: #78350f;
transition: all 0.35s ease;
}
/* 已通關站點 */
.checkpoint-node.passed .node-stone {
background: radial-gradient(circle at 35% 30%, #fef08a 0%, #fbbf24 60%, #d97706 100%);
color: #451a03;
transform: scale(0.9);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
/* 當前站點 */
.checkpoint-node.active .node-stone {
background: radial-gradient(circle at 35% 30%, #ffffff 0%, #ff85a2 50%, #e11d48 100%);
color: white;
border-color: #fff1f2;
transform: scale(1.25);
animation: activeNodePulse 1.6s infinite ease-in-out;
}
@keyframes activeNodePulse {
0%, 100% { box-shadow: 0 0 10px rgba(225, 29, 72, 0.6), 0 0 24px rgba(255, 133, 162, 0.8); }
50% { box-shadow: 0 0 18px rgba(225, 29, 72, 0.9), 0 0 36px rgba(255, 133, 162, 1); }
}
/* 終點城堡站點 */
.checkpoint-node.goal .node-stone {
width: 46px;
height: 46px;
border-radius: 16px;
background: radial-gradient(circle at 30% 30%, #fdf4ff 0%, #c084fc 60%, #7c3aed 100%);
color: white;
font-size: 1.4rem;
border-color: #f3e8ff;
box-shadow: 0 4px 14px rgba(124, 58, 237, 0.45);
}
/* 💖 沿著森林步道前進的卡比 (可點選發出 Poyo 聲音!) */
.kirby-actor {
position: absolute;
z-index: 20;
transform: translate(-50%, -85%);
cursor: pointer;
transition: left 0.75s cubic-bezier(0.34, 1.56, 0.64, 1),
top 0.75s cubic-bezier(0.34, 1.56, 0.64, 1),
transform 0.75s ease;
filter: drop-shadow(0 8px 12px rgba(225, 29, 72, 0.35));
}
.kirby-actor:hover {
filter: drop-shadow(0 0 16px rgba(255, 105, 180, 0.8));
}
.kirby-body-svg {
width: 100%;
height: 100%;
animation: kirbyWalkFloat 1.8s infinite ease-in-out;
pointer-events: none;
}
@keyframes kirbyWalkFloat {
0%, 100% { transform: translateY(0) scale(1, 1); }
50% { transform: translateY(-8px) scale(1.04, 0.96); }
}
/* 答對或被戳戳時的快樂翻跟斗大跳躍 */
.kirby-actor.hop-spin .kirby-body-svg {
animation: kirbyFlipTrick 0.8s ease-out forwards;
}
@keyframes kirbyFlipTrick {
0% { transform: translateY(0) rotate(0deg) scale(1); }
30% { transform: translateY(-46px) rotate(180deg) scale(1.3); }
60% { transform: translateY(-50px) rotate(360deg) scale(1.3); }
85% { transform: translateY(0) rotate(360deg) scale(1.15, 0.9); }
100% { transform: translateY(0) rotate(360deg) scale(1); }
}
/* 卡比說話對話泡泡 (Poyo! 語音氣泡) */
.kirby-bubble {
position: absolute;
top: -34px;
left: 50%;
transform: translateX(-50%) scale(0);
background: white;
color: #e11d48;
font-size: 0.95rem;
font-weight: 900;
padding: 4px 12px;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
border: 2px solid #ffccd5;
white-space: nowrap;
pointer-events: none;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 30;
}
.kirby-bubble::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0;
border-style: solid;
border-color: white transparent;
display: block;
width: 0;
}
.kirby-bubble.active {
transform: translateX(-50%) scale(1.15);
}
/* 點擊卡比飄出的愛心光芒 */
.heart-floater {
position: absolute;
font-size: 1.4rem;
pointer-events: none;
animation: heartFloat 0.9s ease-out forwards;
z-index: 25;
}
@keyframes heartFloat {
0% { opacity: 1; transform: translate(-50%, 0) scale(0.6); }
50% { opacity: 1; transform: translate(-50%, -24px) scale(1.2); }
100% { opacity: 0; transform: translate(-50%, -48px) scale(1); }
}
/* 腳下小星光腳印 */
.footstep-sparkle {
position: absolute;
width: 20px;
height: 20px;
font-size: 1.1rem;
pointer-events: none;
animation: sparkleFade 0.8s ease-out forwards;
z-index: 4;
}
@keyframes sparkleFade {
0% { opacity: 1; transform: scale(0.6) translateY(0); }
100% { opacity: 0; transform: scale(1.4) translateY(-18px); }
}
/* 森林頂部標示牌 */
.forest-signpost {
position: absolute;
top: 14px;
left: 16px;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8px);
padding: 6px 14px;
border-radius: 18px;
border: 2px solid white;
font-size: 0.95rem;
font-weight: 800;
color: #166534;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
z-index: 25;
}
/* =========================================================
📋 精靈算術操作卡片 (Math & Apple Visualizer)
========================================================= */
.math-panel {
width: 100%;
background: var(--card-bg);
backdrop-filter: blur(12px);
border-radius: 28px;
padding: 20px 18px;
border: 4px solid #ffffff;
box-shadow: 0 12px 32px rgba(44, 62, 80, 0.12);
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.math-panel.shake {
animation: gentleShake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}
@keyframes gentleShake {
10%, 90% { transform: translate3d(-3px, 0, 0); }
20%, 80% { transform: translate3d(5px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
40%, 60% { transform: translate3d(6px, 0, 0); }
}
/* 題目算式展示 */
.equation-board {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 3.4rem;
font-weight: 900;
color: #1e293b;
margin-bottom: 12px;
}
.math-num-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 64px;
height: 64px;
padding: 0 16px;
border-radius: 18px;
background: white;
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08);
border: 3px solid #e2e8f0;
color: #0f172a;
}
.math-op-pill {
color: #e11d48;
font-size: 3.6rem;
}
.math-target-pill {
background: #ffe4e6;
border-color: #f43f5e;
color: #e11d48;
animation: pulseMark 1.5s infinite alternate ease-in-out;
}
@keyframes pulseMark {
from { transform: scale(1); }
to { transform: scale(1.08); box-shadow: 0 6px 18px rgba(244, 63, 94, 0.3); }
}
/* 🍎 蘋果實物數數輔助區 */
.apple-aid-box {
width: 100%;
background: linear-gradient(180deg, #f8fcf7 0%, #ecf8ea 100%);
border: 3px dashed #86efac;
border-radius: 20px;
padding: 14px 12px;
margin-bottom: 18px;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.apple-aid-title {
font-size: 1.15rem;
font-weight: 800;
color: #166534;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
flex-wrap: wrap;
}
.apple-aid-subtext {
font-size: 0.9rem;
color: #15803d;
background: rgba(255, 255, 255, 0.9);
padding: 3px 14px;
border-radius: 16px;
border: 1px solid #bbf7d0;
}
.apple-tray {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
width: 100%;
}
.apple-cluster {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
background: rgba(255, 255, 255, 0.8);
padding: 8px 12px;
border-radius: 16px;
border: 2px solid #def0d8;
}
.tray-op {
font-size: 2rem;
font-weight: 900;
color: #059669;
}
/* 單顆蘋果 */
.apple-unit {
position: relative;
width: 50px;
height: 50px;
cursor: pointer;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.apple-unit:hover {
transform: scale(1.16);
}
.apple-unit:active {
transform: scale(0.92);
}
.apple-unit.popped {
animation: appleBounce 0.4s ease-out;
}
@keyframes appleBounce {
0% { transform: scale(1); }
30% { transform: scale(1.3, 0.8); }
60% { transform: scale(0.9, 1.2); }
100% { transform: scale(1); }
}
.apple-unit svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.12));
}
/* 減法扣除紅叉與淡出 */
.apple-unit.subtracted {
opacity: 0.32;
filter: grayscale(35%);
}
.apple-unit.subtracted .cross-icon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
/* 點擊蘋果彈出數字氣泡 */
.apple-pop-bubble {
position: absolute;
top: -14px;
left: 50%;
transform: translateX(-50%);
background: #ff4757;
color: white;
font-size: 0.85rem;
font-weight: 900;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
border: 2px solid white;
animation: floatFadeUp 0.8s ease-out forwards;
pointer-events: none;
z-index: 10;
}
@keyframes floatFadeUp {
0% { opacity: 0; transform: translate(-50%, 6px) scale(0.6); }
40% { opacity: 1; transform: translate(-50%, -8px) scale(1.15); }
100% { opacity: 0; transform: translate(-50%, -24px) scale(1); }
}
/* 底部 4 個可愛大單選選項 */
.options-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
width: 100%;
max-width: 660px;
}
.candy-btn {
position: relative;
border: none;
outline: none;
border-radius: 22px;
padding: 16px 8px;
font-size: 2.5rem;
font-weight: 900;
color: white;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
box-shadow: 0 8px 0 var(--btn-shadow), 0 14px 20px rgba(0, 0, 0, 0.14);
transition: all 0.12s ease;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.candy-btn:hover {
transform: translateY(-4px);
filter: brightness(1.06);
}
.candy-btn:active {
transform: translateY(6px);
box-shadow: 0 2px 0 var(--btn-shadow), 0 6px 10px rgba(0, 0, 0, 0.1);
}
.btn-theme-0 {
background: linear-gradient(145deg, #ff6b81, #ee5253);
--btn-shadow: #c0392b;
}
.btn-theme-1 {
background: linear-gradient(145deg, #ffa502, #ff7f50);
--btn-shadow: #d35400;
}
.btn-theme-2 {
background: linear-gradient(145deg, #2ed573, #10ac84);
--btn-shadow: #1e824c;
}
.btn-theme-3 {
background: linear-gradient(145deg, #1e90ff, #3742fa);
--btn-shadow: #2f3542;
}
.candy-btn-sub {
font-size: 0.85rem;
font-weight: 700;
opacity: 0.9;
letter-spacing: 0.5px;
}
/* 鼓勵提示 Toast */
.toast-banner {
position: fixed;
bottom: 24px;
background: rgba(30, 41, 59, 0.92);
color: white;
padding: 10px 24px;
border-radius: 50px;
font-weight: 800;
font-size: 1.1rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(8px);
z-index: 999;
opacity: 0;
transform: translateY(20px);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
}
.toast-banner.show {
opacity: 1;
transform: translateY(0);
}
/* 飛舞金幣 */
.flying-coins-box {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1000;
}
.flying-coin-item {
position: absolute;
width: 32px;
height: 32px;
font-size: 1.9rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.75s cubic-bezier(0.2, 0.8, 0.2, 1);
z-index: 1001;
}
/* 慶典彩帶 Canvas */
#fxCanvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 800;
}
/* 🏆 通關結算滿版彈窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.78);
backdrop-filter: blur(10px);
display: flex;
align-items: center;
justify-content: center;
z-index: 900;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s ease;
padding: 16px;
}
.modal-overlay.active {
opacity: 1;
pointer-events: auto;
}
.victory-box {
background: white;
border-radius: 36px;
padding: 36px 28px 30px;
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.35), 0 0 0 8px #ffd166;
transform: scale(0.8) translateY(30px);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
}
.modal-overlay.active .victory-box {
transform: scale(1) translateY(0);
}
.trophy-glow {
position: absolute;
top: 25%;
left: 50%;
width: 140px;
height: 140px;
background: radial-gradient(circle, #fde047 20%, rgba(253, 224, 71, 0) 70%);
transform: translate(-50%, -50%);
border-radius: 50%;
animation: rotateLight 8s linear infinite;
z-index: 0;
}
@keyframes rotateLight {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.trophy-symbol {
font-size: 5.5rem;
position: relative;
z-index: 1;
animation: trophyJump 1.8s infinite ease-in-out;
}
@keyframes trophyJump {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px) scale(1.05); }
}
.victory-h1 {
font-size: 2.2rem;
font-weight: 900;
color: #b45309;
margin-top: 10px;
margin-bottom: 6px;
}
.victory-desc {
font-size: 1.15rem;
font-weight: 800;
color: #4b5563;
margin-bottom: 20px;
}
.stats-grid {
width: 100%;
background: #fffbeb;
border: 2px solid #fef3c7;
border-radius: 20px;
padding: 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 22px;
}
.stat-cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-num {
font-size: 1.8rem;
font-weight: 900;
color: #92400e;
}
.stat-tit {
font-size: 0.85rem;
font-weight: 700;
color: #b45309;
}
.honor-tag {
background: linear-gradient(135deg, #ec4899, #f43f5e);
color: white;
padding: 6px 18px;
border-radius: 50px;
font-size: 1.05rem;
font-weight: 800;
margin-bottom: 22px;
box-shadow: 0 4px 14px rgba(244, 63, 94, 0.35);
}
.btn-replay {
background: linear-gradient(135deg, #10b981, #059669);
border: none;
outline: none;
color: white;
font-size: 1.4rem;
font-weight: 900;
padding: 16px 36px;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 8px 0 #047857, 0 14px 24px rgba(16, 185, 129, 0.35);
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 10px;
}
.btn-replay:hover {
transform: translateY(-3px);
filter: brightness(1.08);
}
.btn-replay:active {
transform: translateY(4px);
box-shadow: 0 2px 0 #047857;
}
/* 響應式優化 */
@media (max-width: 640px) {
.equation-board {
font-size: 2.6rem;
gap: 6px;
}
.math-num-pill {
min-width: 52px;
height: 52px;
font-size: 2.3rem;
}
.math-op-pill {
font-size: 2.6rem;
}
.options-row {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.candy-btn {
padding: 14px 6px;
font-size: 2.1rem;
}
.node-stone {
width: 32px;
height: 32px;
font-size: 0.8rem;
}
.checkpoint-node.goal .node-stone {
width: 38px;
height: 38px;
font-size: 1.15rem;
}
.apple-unit {
width: 44px;
height: 44px;
}
}
</style>
</head>
<body>
<!-- 彩帶星星雨 Canvas -->
<canvas id="fxCanvas"></canvas>
<!-- 飛舞金幣層 -->
<div class="flying-coins-box" id="flyingCoinsBox"></div>
<!-- 頂部 HUD (寶箱、連勝、控制鍵) -->
<header class="top-hud">
<div class="hud-left">
<div class="hud-badge badge-coins" id="coinBadge">
<span>🪙</span>
<span id="coinVal">0</span>
</div>
<div class="hud-badge badge-streak" id="streakBadge">
<span>🔥</span>
<span>連對 <span id="streakVal">0</span> 題</span>
</div>
<div class="hud-badge badge-stage">
<span>🚩</span>
<span id="stageBadge">第 1 / 10 關</span>
</div>
</div>
<div class="hud-right">
<button class="icon-btn kirby-voice-btn" id="btnPoyoTalk" title="點我聽卡比說話">
<span>💖</span>
<span>波喲!</span>
</button>
<button class="icon-btn" id="btnSoundToggle" title="切換音效">🔊</button>
<button class="icon-btn" id="btnQuickReset" title="重新開始">🔄</button>
</div>
</header>
<!-- 主遊戲舞台 -->
<div class="game-viewport">
<!-- 🌲 沉浸式森林探險小徑 (Forest Adventure Stage) -->
<section class="forest-stage" id="forestStage">
<!-- 標示牌 -->
<div class="forest-signpost">
<span>🌲 夢幻精靈森林小徑</span>
<span id="trailStatusText">・點點看卡比會說話喔!💖</span>
</div>
<!-- 10 個沿著蜿蜒步道的探索踏石站點 -->
<div id="checkpointsContainer"></div>
<!-- 💖 在森林小徑上前進探索的卡比主角 (可點選互動) -->
<div class="kirby-actor" id="kirbyActor" title="點點我!卡比會說話唱歌喔!">
<div class="kirby-bubble" id="kirbyBubble">波喲!💖</div>
<svg viewBox="0 0 100 100" class="kirby-body-svg">
<defs>
<radialGradient id="kirbyPinkGrad" cx="35%" cy="30%" r="65%">
<stop offset="0%" stop-color="#ffe6ef"/>
<stop offset="65%" stop-color="#ff9ec3"/>
<stop offset="100%" stop-color="#f0729f"/>
</radialGradient>
</defs>
<!-- 左紅鞋 -->
<ellipse cx="28" cy="85" rx="16" ry="10" fill="#e52555" transform="rotate(-12 28 85)"/>
<!-- 右紅鞋 -->
<ellipse cx="72" cy="85" rx="16" ry="10" fill="#e52555" transform="rotate(12 72 85)"/>
<!-- 左手 -->
<ellipse cx="14" cy="54" rx="12" ry="9" fill="#ffb4cf" transform="rotate(-30 14 54)"/>
<!-- 右手 -->
<ellipse cx="86" cy="54" rx="12" ry="9" fill="#ffb4cf" transform="rotate(30 86 54)"/>
<!-- 圓身 -->
<circle cx="50" cy="50" r="38" fill="url(#kirbyPinkGrad)"/>
<!-- 左眼 -->
<ellipse cx="40" cy="42" rx="4.5" ry="10" fill="#15274d"/>
<ellipse cx="40" cy="38" rx="3" ry="5.5" fill="#ffffff"/>
<ellipse cx="40" cy="47" rx="3.5" ry="4" fill="#2d68c4"/>
<!-- 右眼 -->
<ellipse cx="60" cy="42" rx="4.5" ry="10" fill="#15274d"/>
<ellipse cx="60" cy="38" rx="3" ry="5.5" fill="#ffffff"/>
<ellipse cx="60" cy="47" rx="3.5" ry="4" fill="#2d68c4"/>
<!-- 腮紅 -->
<ellipse cx="28" cy="55" rx="7" ry="4" fill="#ff6897" opacity="0.85"/>
<ellipse cx="72" cy="55" rx="7" ry="4" fill="#ff6897" opacity="0.85"/>
<!-- 微笑 -->
<path d="M 43 54 Q 50 65 57 54 Z" fill="#9b1338"/>
<path d="M 45 58 Q 50 64 55 58 Z" fill="#ff5376"/>
</svg>
</div>
</section>
<!-- 📋 精靈算術操作卡片 (Math & Apple Visualizer) -->
<main class="math-panel" id="mathPanel">
<!-- 數學算式展示 -->
<div class="equation-board">
<div class="math-num-pill" id="numLeft">3</div>
<div class="math-op-pill" id="opSymbol">+</div>
<div class="math-num-pill" id="numRight">2</div>
<div class="math-op-pill">=</div>
<div class="math-num-pill math-target-pill">?</div>
</div>
<!-- 🍎 蘋果輔助數數區 -->
<section class="apple-aid-box">
<div class="apple-aid-title" id="applePrompt">
<span>🍎 蘋果數數小精靈</span>
</div>
<div class="apple-tray" id="appleTray"></div>
<div class="apple-aid-subtext">💡 點點看蘋果,可以一顆顆幫忙數數喔!</div>
</section>
<!-- 4 個大答題選項按鈕 -->
<div class="options-row" id="optionsRow"></div>
</main>
</div>
<!-- 提示訊息條 -->
<div class="toast-banner" id="toastBanner">提示訊息</div>
<!-- 🏆 通關結算滿版彈窗 -->
<div class="modal-overlay" id="victoryModal">
<div class="victory-box">
<div class="trophy-glow"></div>
<div class="trophy-symbol">🏆</div>
<h1 class="victory-h1">森林大闖關成功!</h1>
<p class="victory-desc">卡比跳著勝利之舞,成功抵達精靈城堡囉!💖🏰</p>
<div class="stats-grid">
<div class="stat-cell">
<div class="stat-num" id="endCoins">0</div>
<div class="stat-tit">🪙 獲得金幣</div>
</div>
<div class="stat-cell">
<div class="stat-num" id="endStreak">0</div>
<div class="stat-tit">🔥 最高連對</div>
</div>
</div>
<div class="honor-tag">🌟 榮譽稱號:夢幻森林小小數學王 ⭐⭐⭐⭐⭐</div>
<button class="btn-replay" id="btnReplay">
<span>再玩一次</span>
<span>🔄</span>
</button>
</div>
</div>
<script>
/* =========================================================
1. 任天堂 Switch 星之卡比官方風格純代碼音效與語音合成引擎
(Nintendo Switch Kirby Audio & Voice Synthesizer)
========================================================= */
class SwitchKirbyAudioEngine {
constructor() {
this.ctx = null;
this.enabled = true;
}
initContext() {
if (!this.ctx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
this.ctx = new AudioContext();
}
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
toggleAudio() {
this.enabled = !this.enabled;
return this.enabled;
}
// 💖 1. 卡比招牌萌語音:「波喲!」(Poyo! 語音共鳴合成)
playKirbyPoyo() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
// Vowel Formant Synthesis for "PO-YO!"
// "P" 爆破輔音
const pBuffer = this.ctx.createBuffer(1, Math.floor(this.ctx.sampleRate * 0.022), this.ctx.sampleRate);
const pData = pBuffer.getChannelData(0);
for (let i = 0; i < pData.length; i++) {
pData[i] = (Math.random() * 2 - 1) * Math.exp(-i / (this.ctx.sampleRate * 0.005));
}
const pSrc = this.ctx.createBufferSource();
pSrc.buffer = pBuffer;
const pFilt = this.ctx.createBiquadFilter();
pFilt.type = 'bandpass';
pFilt.frequency.setValueAtTime(620, now);
pFilt.Q.setValueAtTime(3.2, now);
const pGain = this.ctx.createGain();
pGain.gain.setValueAtTime(0.25, now);
pSrc.connect(pFilt);
pFilt.connect(pGain);
pGain.connect(this.ctx.destination);
pSrc.start(now);
// "O-YO" 母音主音源 (雙振盪器合成)
const osc1 = this.ctx.createOscillator();
const osc2 = this.ctx.createOscillator();
osc1.type = 'triangle';
osc2.type = 'sine';
// 卡比招牌俏皮音調滑動: 480Hz -> 610Hz -> 510Hz -> 540Hz
[osc1, osc2].forEach(osc => {
osc.frequency.setValueAtTime(480, now);
osc.frequency.exponentialRampToValueAtTime(620, now + 0.08); // Po 揚起
osc.frequency.exponentialRampToValueAtTime(510, now + 0.17); // Yo 轉折
osc.frequency.exponentialRampToValueAtTime(540, now + 0.27);
});
// 雙共振峰濾波器 (模擬可愛口腔共鳴)
const f1 = this.ctx.createBiquadFilter();
f1.type = 'bandpass';
f1.frequency.setValueAtTime(530, now);
f1.Q.setValueAtTime(4.2, now);
const f2 = this.ctx.createBiquadFilter();
f2.type = 'bandpass';
f2.frequency.setValueAtTime(1020, now);
f2.Q.setValueAtTime(4.8, now);
const vGain = this.ctx.createGain();
vGain.gain.setValueAtTime(0.001, now);
vGain.gain.linearRampToValueAtTime(0.32, now + 0.025);
vGain.gain.setValueAtTime(0.30, now + 0.14);
vGain.gain.exponentialRampToValueAtTime(0.001, now + 0.32);
osc1.connect(f1);
osc2.connect(f2);
f1.connect(vGain);
f2.connect(vGain);
vGain.connect(this.ctx.destination);
osc1.start(now);
osc2.start(now);
osc1.stop(now + 0.32);
osc2.stop(now + 0.32);
}
// 💖 2. 卡比打招呼萌語音:「嗨~!」(Hi! / はーい!)
playKirbyHi() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
osc.type = 'triangle';
// 萌萌高音向上揚: 560Hz -> 760Hz
osc.frequency.setValueAtTime(560, now);
osc.frequency.exponentialRampToValueAtTime(780, now + 0.22);
const f1 = this.ctx.createBiquadFilter();
f1.type = 'bandpass';
// 母音從 /a/ 滑向 /i/
f1.frequency.setValueAtTime(740, now);
f1.frequency.exponentialRampToValueAtTime(390, now + 0.22);
f1.Q.setValueAtTime(4, now);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0.01, now);
gain.gain.linearRampToValueAtTime(0.3, now + 0.03);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.26);
osc.connect(f1);
f1.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.26);
}
// ⭐ 3. Switch《星之卡比 探索發現》星星硬幣 (Star Coin) 水晶清脆鈴聲
playSwitchStarCoin(delay = 0) {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime + delay;
// 水晶鈴音三和弦層疊 (C6, G6, C7)
const notes = [
{ f: 1046.50, t: 0.00, d: 0.32 },
{ f: 1567.98, t: 0.04, d: 0.38 },
{ f: 2093.00, t: 0.08, d: 0.45 }
];
notes.forEach(n => {
const osc = this.ctx.createOscillator();
const mod = this.ctx.createOscillator();
const modGain = this.ctx.createGain();
const gain = this.ctx.createGain();
osc.type = 'sine';
mod.type = 'sine';
mod.frequency.setValueAtTime(n.f * 2.2, now + n.t); // FM 水晶泛音
modGain.gain.setValueAtTime(n.f * 0.35, now + n.t);
modGain.gain.exponentialRampToValueAtTime(1, now + n.t + 0.12);
mod.connect(modGain);
modGain.connect(osc.frequency);
osc.frequency.setValueAtTime(n.f, now + n.t);
gain.gain.setValueAtTime(0.18, now + n.t);
gain.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
osc.connect(gain);
gain.connect(this.ctx.destination);
mod.start(now + n.t);
osc.start(now + n.t);
mod.stop(now + n.t + n.d);
osc.stop(now + n.t + n.d);
});
}
// ✨ 4. Switch 傳送之星閃耀衝刺音 (Warp Star Sparkle)
playWarpStarWhoosh() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const starNotes = [1318.51, 1567.98, 1760.00, 2093.00, 2637.02];
starNotes.forEach((freq, idx) => {
const t = now + idx * 0.035;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, t);
gain.gain.setValueAtTime(0.14, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.22);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(t);
osc.stop(t + 0.22);
});
}
// 🐾 5. 卡比跳躍浮空音 (Boing-Puff)
playKirbyHopSound() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(360, now);
osc.frequency.exponentialRampToValueAtTime(680, now + 0.12);
gain.gain.setValueAtTime(0.24, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.14);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.14);
}
// ❓ 6. 答錯時卡比疑惑輕呼聲:「波?」(柔和無挫折提示音)
playKirbyDoubt() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const f1 = this.ctx.createBiquadFilter();
const gain = this.ctx.createGain();
osc.type = 'triangle';
// 疑問音調微揚: 340Hz -> 420Hz
osc.frequency.setValueAtTime(340, now);
osc.frequency.exponentialRampToValueAtTime(430, now + 0.18);
f1.type = 'bandpass';
f1.frequency.setValueAtTime(550, now);
f1.Q.setValueAtTime(3.5, now);
gain.gain.setValueAtTime(0.18, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.22);
osc.connect(f1);
f1.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.22);
}
// 🍎 7. 點擊蘋果泡泡啵聲
playApplePop(step = 1) {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'sine';
const baseFreq = 480 + (step % 10) * 45;
osc.frequency.setValueAtTime(baseFreq, now);
osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.5, now + 0.09);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.09);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now);
osc.stop(now + 0.09);
}
// 🎵 8. 答對時的卡比快樂過關短樂句 (Kirby Mini Celebration)
playCorrectCelebration() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
// 先播放星星硬幣聲,再接卡比 Poyo 語音!
this.playSwitchStarCoin(0);
setTimeout(() => this.playKirbyPoyo(), 120);
// 搭配經典 Green Greens 活力音階
const melody = [
{ f: 523.25, t: 0.18, d: 0.08 }, // C5
{ f: 659.25, t: 0.26, d: 0.08 }, // E5
{ f: 783.99, t: 0.34, d: 0.08 }, // G5
{ f: 1046.50, t: 0.42, d: 0.16 } // C6
];
melody.forEach(n => {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(n.f, now + n.t);
gain.gain.setValueAtTime(0.22, now + n.t);
gain.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start(now + n.t);
osc.stop(now + n.t + n.d);
});
}
// 🏆 9. 經典「卡比勝利之舞」(Kirby Dance Fanfare - 全系列破關招牌曲)
playKirbyDanceTheme() {
if (!this.enabled) return;
this.initContext();
if (!this.ctx) return;
const now = this.ctx.currentTime;
// 官方經典三段式卡比跳舞旋律 (D 大調)
const danceNotes = [
// 第 1 步 (左踏步)
{ f: 587.33, t: 0.00, d: 0.10 }, // D5
{ f: 739.99, t: 0.10, d: 0.10 }, // F#5
{ f: 880.00, t: 0.20, d: 0.10 }, // A5
{ f: 1174.66, t: 0.30, d: 0.24 },// D6
// 第 2 步 (右踏步)
{ f: 1108.73, t: 0.58, d: 0.10 },// C#6
{ f: 987.77, t: 0.68, d: 0.10 }, // B5
{ f: 880.00, t: 0.78, d: 0.10 }, // A5
{ f: 739.99, t: 0.88, d: 0.24 }, // F#5
// 第 3 步 (歡樂轉圈翻跟斗)
{ f: 783.99, t: 1.16, d: 0.09 }, // G5
{ f: 880.00, t: 0.09 + 1.16, d: 0.09 }, // A5
{ f: 987.77, t: 0.18 + 1.16, d: 0.09 }, // B5
{ f: 783.99, t: 0.27 + 1.16, d: 0.09 }, // G5
{ f: 880.00, t: 0.36 + 1.16, d: 0.09 }, // A5
{ f: 987.77, t: 0.45 + 1.16, d: 0.09 }, // B5
{ f: 1108.73, t: 0.54 + 1.16, d: 0.14 },// C#6
// 終曲定格亮亮相!(D6 長大調和弦)
{ f: 1174.66, t: 1.88, d: 0.65 } // D6
];
danceNotes.forEach(n => {
// 主音 (Triangle 圓潤管樂)
const osc1 = this.ctx.createOscillator();
const gain1 = this.ctx.createGain();
osc1.type = 'triangle';
osc1.frequency.setValueAtTime(n.f, now + n.t);
gain1.gain.setValueAtTime(0.24, now + n.t);
gain1.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
osc1.connect(gain1);
gain1.connect(this.ctx.destination);
osc1.start(now + n.t);
osc1.stop(now + n.t + n.d);
// 輔助泛音 (Square 活潑跳躍音)
const osc2 = this.ctx.createOscillator();
const gain2 = this.ctx.createGain();
osc2.type = 'square';
osc2.frequency.setValueAtTime(n.f, now + n.t);
gain2.gain.setValueAtTime(0.12, now + n.t);
gain2.gain.exponentialRampToValueAtTime(0.001, now + n.t + n.d);
osc2.connect(gain2);
gain2.connect(this.ctx.destination);
osc2.start(now + n.t);
osc2.stop(now + n.t + n.d);
});
// 終曲伴隨卡比興奮的「波喲!YAY!」
setTimeout(() => {
this.playKirbyPoyo();
this.playWarpStarWhoosh();
}, 2200);
}
}
const sound = new SwitchKirbyAudioEngine();
/* =========================================================
2. 蘋果與紅叉 SVG 生成
========================================================= */
function renderAppleSvg(type = 'red') {
const isGreen = type === 'green';
const c1 = isGreen ? '#84cc16' : '#ef4444';
const c2 = isGreen ? '#4d7c0f' : '#b91c1c';
const hl = isGreen ? '#d9f99d' : '#fca5a5';
return `
<svg viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
<path d="M 28 16 Q 30 7 38 6" stroke="#78350f" stroke-width="3.5" fill="none" stroke-linecap="round"/>
<path d="M 32 12 Q 44 8 42 18 Q 33 18 32 12 Z" fill="#22c55e"/>
<path d="M 30 18 C 18 16, 8 26, 9 38 C 10 49, 22 55, 30 52 C 38 55, 50 49, 51 38 C 52 26, 42 16, 30 18 Z" fill="${c1}"/>
<path d="M 16 43 C 24 53, 36 53, 44 43 C 41 51, 19 51, 16 43 Z" fill="${c2}" opacity="0.4"/>
<ellipse cx="20" cy="28" rx="4.5" ry="8" fill="${hl}" transform="rotate(-20 20 28)" opacity="0.8"/>
</svg>
`;
}
function renderCrossSvg() {
return `
<svg class="cross-icon" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
<circle cx="30" cy="35" r="22" fill="rgba(255, 255, 255, 0.78)"/>
<line x1="18" y1="23" x2="42" y2="47" stroke="#dc2626" stroke-width="6.5" stroke-linecap="round"/>
<line x1="42" y1="23" x2="18" y2="47" stroke="#dc2626" stroke-width="6.5" stroke-linecap="round"/>
</svg>
`;
}
/* =========================================================
3. 森林探險小徑 10 個站點座標映射 (完全精確對齊背景圖林間步道)
========================================================= */
const TRAIL_CHECKPOINTS = [
{ step: 1, x: 13, y: 88, scale: 1.18, label: "1" }, // 前景起點 (近左)
{ step: 2, x: 27, y: 89, scale: 1.14, label: "2" }, // 林間下道 (中左)
{ step: 3, x: 43, y: 90, scale: 1.10, label: "3" }, // 林間下道 (正中)
{ step: 4, x: 60, y: 88, scale: 1.05, label: "4" }, // 穿過微笑紫菇 (中右)
{ step: 5, x: 77, y: 86, scale: 0.98, label: "5" }, // 藍色笑臉菇旁 (轉折點)
{ step: 6, x: 69, y: 76, scale: 0.88, label: "6" }, // 沿著小丘蜿蜒攀升
{ step: 7, x: 55, y: 73, scale: 0.80, label: "7" }, // 穿過林蔭中央小坡
{ step: 8, x: 43, y: 69, scale: 0.72, label: "8" }, // 微笑黃樹前步道
{ step: 9, x: 48, y: 63, scale: 0.65, label: "9" }, // 迎向遠方群山山谷
{ step: 10, x: 52, y: 56, scale: 0.58, label: "🏰", isGoal: true } // 終點!精靈城堡
];
/* =========================================================
4. 彩帶與五角星雨 Canvas 特效
========================================================= */
const fxCanvas = document.getElementById('fxCanvas');
const fxCtx = fxCanvas.getContext('2d');
let fxParticles = [];
let fxAnimId = null;
function resizeFx() {
fxCanvas.width = window.innerWidth;
fxCanvas.height = window.innerHeight;
}
window.addEventListener('resize', resizeFx);
resizeFx();
function triggerConfetti(count = 45, isMassive = false) {
const colors = ['#ff4757', '#ffa502', '#2ed573', '#1e90ff', '#9b59b6', '#ffd32a', '#ff9ff3'];
const total = isMassive ? 160 : count;
for (let i = 0; i < total; i++) {
fxParticles.push({
x: isMassive ? Math.random() * fxCanvas.width : fxCanvas.width * 0.5 + (Math.random() - 0.5) * 280,
y: isMassive ? Math.random() * -80 : fxCanvas.height * 0.4,
vx: (Math.random() - 0.5) * (isMassive ? 14 : 9),
vy: isMassive ? Math.random() * 5 + 3 : -(Math.random() * 8 + 4),
size: Math.random() * 8 + 6,
color: colors[Math.floor(Math.random() * colors.length)],
rotation: Math.random() * 360,
rotSpeed: (Math.random() - 0.5) * 10,
shape: Math.random() > 0.4 ? 'star' : 'rect',
gravity: 0.28,
alpha: 1,
decay: Math.random() * 0.015 + 0.008
});
}
if (!fxAnimId) {
animateFx();
}
}
function drawStarShape(cx, cy, spikes, outerR, innerR, fill, alpha) {
let rot = (Math.PI / 2) * 3;
let x = cx;
let y = cy;
const step = Math.PI / spikes;
fxCtx.save();
fxCtx.globalAlpha = alpha;
fxCtx.beginPath();
fxCtx.moveTo(cx, cy - outerR);
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerR;
y = cy + Math.sin(rot) * outerR;
fxCtx.lineTo(x, y);
rot += step;
x = cx + Math.cos(rot) * innerR;
y = cy + Math.sin(rot) * innerR;
fxCtx.lineTo(x, y);
rot += step;
}
fxCtx.lineTo(cx, cy - outerR);
fxCtx.closePath();
fxCtx.fillStyle = fill;
fxCtx.fill();
fxCtx.restore();
}
function animateFx() {
fxCtx.clearRect(0, 0, fxCanvas.width, fxCanvas.height);
for (let i = fxParticles.length - 1; i >= 0; i--) {
const p = fxParticles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.rotation += p.rotSpeed;
p.alpha -= p.decay;
if (p.alpha <= 0 || p.y > fxCanvas.height + 40) {
fxParticles.splice(i, 1);
continue;
}
if (p.shape === 'star') {
drawStarShape(p.x, p.y, 5, p.size, p.size / 2, p.color, p.alpha);
} else {
fxCtx.save();
fxCtx.globalAlpha = p.alpha;
fxCtx.translate(p.x, p.y);
fxCtx.rotate((p.rotation * Math.PI) / 180);
fxCtx.fillStyle = p.color;
fxCtx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.6);
fxCtx.restore();
}
}
if (fxParticles.length > 0) {
fxAnimId = requestAnimationFrame(animateFx);
} else {
fxAnimId = null;
}
}
/* =========================================================
5. 飛耀金幣與腳印星光
========================================================= */
function launchFlyingCoins(fromEl) {
const fromRect = fromEl.getBoundingClientRect();
const toRect = document.getElementById('coinBadge').getBoundingClientRect();
const container = document.getElementById('flyingCoinsBox');
const sX = fromRect.left + fromRect.width / 2;
const sY = fromRect.top + fromRect.height / 2;
const eX = toRect.left + toRect.width / 2;
const eY = toRect.top + toRect.height / 2;
for (let i = 0; i < 4; i++) {
const c = document.createElement('div');
c.className = 'flying-coin-item';
c.innerHTML = '🪙';
c.style.left = `${sX - 16}px`;
c.style.top = `${sY - 16}px`;
container.appendChild(c);
setTimeout(() => {
c.style.transform = `translate(${eX - sX}px, ${eY - sY}px) scale(0.65)`;
c.style.opacity = '0.9';
sound.playSwitchStarCoin(0.12);
setTimeout(() => {
c.remove();
const badge = document.getElementById('coinBadge');
badge.classList.remove('bounce');
void badge.offsetWidth;
badge.classList.add('bounce');
}, 650);
}, i * 80);
}
}
function leaveFootstepSparkle(xPercent, yPercent) {
const stage = document.getElementById('forestStage');
const sparkle = document.createElement('div');
sparkle.className = 'footstep-sparkle';
sparkle.innerHTML = '✨';
sparkle.style.left = `${xPercent}%`;
sparkle.style.top = `${yPercent}%`;
stage.appendChild(sparkle);
setTimeout(() => sparkle.remove(), 800);
}
// 觸發卡比可愛對話氣泡與愛心
function triggerKirbyChat(text = "波喲!💖") {
const bubble = document.getElementById('kirbyBubble');
bubble.textContent = text;
bubble.classList.add('active');
const actor = document.getElementById('kirbyActor');
const heart = document.createElement('div');
heart.className = 'heart-floater';
heart.innerHTML = Math.random() > 0.5 ? '💖' : '⭐';
heart.style.left = '50%';
heart.style.top = '-20px';
actor.appendChild(heart);
setTimeout(() => heart.remove(), 900);
setTimeout(() => bubble.classList.remove('active'), 1200);
}
/* =========================================================
6. 遊戲狀態與森林冒險核心邏輯
========================================================= */
const MAX_STAGES = 10;
const appState = {
stage: 1,
coins: 0,
streak: 0,
maxStreak: 0,
currentQ: null,
isLocked: false,
appleTaps: 0
};
function renderCheckpoints() {
const container = document.getElementById('checkpointsContainer');
container.innerHTML = '';
TRAIL_CHECKPOINTS.forEach(pt => {
const node = document.createElement('div');
node.className = `checkpoint-node ${pt.isGoal ? 'goal' : ''}`;
node.id = `ckpt-${pt.step}`;
node.style.left = `${pt.x}%`;
node.style.top = `${pt.y}%`;
node.innerHTML = `
<div class="node-stone">${pt.label}</div>
`;
container.appendChild(node);
});
updateKirbyOnTrail(false);
}
function updateKirbyOnTrail(animate = true) {
const currentPt = TRAIL_CHECKPOINTS[appState.stage - 1];
const kirby = document.getElementById('kirbyActor');
if (!currentPt || !kirby) return;
const baseSize = 62;
const targetSize = baseSize * currentPt.scale;
kirby.style.width = `${targetSize}px`;
kirby.style.height = `${targetSize}px`;
kirby.style.transition = animate
? 'left 0.75s cubic-bezier(0.34, 1.56, 0.64, 1), top 0.75s cubic-bezier(0.34, 1.56, 0.64, 1)'
: 'none';
kirby.style.left = `${currentPt.x}%`;
kirby.style.top = `${currentPt.y}%`;
if (animate) {
leaveFootstepSparkle(currentPt.x, currentPt.y);
}
TRAIL_CHECKPOINTS.forEach(pt => {
const node = document.getElementById(`ckpt-${pt.step}`);
if (!node) return;
node.classList.remove('active', 'passed');
const stone = node.querySelector('.node-stone');
if (pt.step < appState.stage) {
node.classList.add('passed');
if (!pt.isGoal) stone.innerHTML = '⭐';
} else if (pt.step === appState.stage) {
node.classList.add('active');
if (!pt.isGoal) stone.innerHTML = `${pt.step}`;
} else {
if (!pt.isGoal) stone.innerHTML = `${pt.step}`;
}
});
document.getElementById('stageBadge').textContent = `第 ${appState.stage} / ${MAX_STAGES} 關`;
const statusText = appState.stage === 10 ? "・抵達精靈城堡!🏰" : `・前進到第 ${appState.stage} 站 🐾`;
document.getElementById('trailStatusText').textContent = statusText;
}
function createMathQuestion() {
const isAdd = Math.random() >= 0.5;
let num1, num2, answer, operator;
if (isAdd) {
operator = '+';
answer = Math.floor(Math.random() * 9) + 2;
num1 = Math.floor(Math.random() * (answer - 1)) + 1;
num2 = answer - num1;
} else {
operator = '-';
num1 = Math.floor(Math.random() * 9) + 2;
num2 = Math.floor(Math.random() * (num1 - 1)) + 1;
answer = num1 - num2;
}
const pool = [];
for (let i = 0; i <= 10; i++) {
if (i !== answer) pool.push(i);
}
pool.sort((a, b) => Math.abs(a - answer) - Math.abs(b - answer) || (Math.random() - 0.5));
const distractors = pool.slice(0, 3);
const options = [answer, ...distractors].sort(() => Math.random() - 0.5);
return { num1, num2, operator, answer, options };
}
function displayQuestion(q) {
appState.currentQ = q;
appState.isLocked = false;
appState.appleTaps = 0;
document.getElementById('numLeft').textContent = q.num1;
document.getElementById('opSymbol').textContent = q.operator;
document.getElementById('numRight').textContent = q.num2;
const appleTray = document.getElementById('appleTray');
const prompt = document.getElementById('applePrompt');
appleTray.innerHTML = '';
if (q.operator === '+') {
prompt.innerHTML = `
<span>🍎 ${q.num1} 顆紅蘋果</span>
<span>➕</span>
<span>🍏 ${q.num2} 顆青蘋果,合起來有幾顆呢?</span>
`;
const grp1 = document.createElement('div');
grp1.className = 'apple-cluster';
for (let i = 1; i <= q.num1; i++) {
grp1.appendChild(buildAppleUnit('red', i));
}
const opDiv = document.createElement('div');
opDiv.className = 'tray-op';
opDiv.textContent = '➕';
const grp2 = document.createElement('div');
grp2.className = 'apple-cluster';
for (let i = 1; i <= q.num2; i++) {
grp2.appendChild(buildAppleUnit('green', q.num1 + i));
}
appleTray.appendChild(grp1);
appleTray.appendChild(opDiv);
appleTray.appendChild(grp2);
} else {
const remain = q.num1 - q.num2;
prompt.innerHTML = `
<span>🍎 原本有 ${q.num1} 顆蘋果</span>
<span>➖</span>
<span>拿走扣除 ${q.num2} 顆(打叉叉 ❌),還剩下幾顆呢?</span>
`;
const grp = document.createElement('div');
grp.className = 'apple-cluster';
for (let i = 1; i <= q.num1; i++) {
const isTaken = i > remain;
grp.appendChild(buildAppleUnit('red', i, isTaken));
}
appleTray.appendChild(grp);
}
displayOptions(q.options);
}
function buildAppleUnit(color, count, isSub = false) {
const unit = document.createElement('div');
unit.className = `apple-unit ${isSub ? 'subtracted' : ''}`;
unit.innerHTML = renderAppleSvg(color);
if (isSub) {
unit.insertAdjacentHTML('beforeend', renderCrossSvg());
unit.title = "扣除的蘋果 ❌";
}
unit.addEventListener('click', () => {
unit.classList.remove('popped');
void unit.offsetWidth;
unit.classList.add('popped');
appState.appleTaps++;
sound.playApplePop(appState.appleTaps);
const bubble = document.createElement('div');
bubble.className = 'apple-pop-bubble';
bubble.textContent = count;
unit.appendChild(bubble);
setTimeout(() => bubble.remove(), 800);
});
return unit;
}
function displayOptions(options) {
const row = document.getElementById('optionsRow');
row.innerHTML = '';
options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.className = `candy-btn btn-theme-${idx % 4}`;
btn.innerHTML = `
<span>${opt}</span>
<span class="candy-btn-sub">點我回答</span>
`;
btn.addEventListener('click', () => onChooseAnswer(opt, btn));
row.appendChild(btn);
});
}
let toastTimer = null;
function notify(msg) {
const t = document.getElementById('toastBanner');
t.textContent = msg;
t.classList.add('show');
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 1900);
}
// 作答判定
function onChooseAnswer(selected, btnEl) {
if (appState.isLocked) return;
const q = appState.currentQ;
if (selected === q.answer) {
// === 答對!Switch 卡比音效與前進 ===
appState.isLocked = true;
appState.coins += 10;
appState.streak += 1;
if (appState.streak > appState.maxStreak) {
appState.maxStreak = appState.streak;
}
document.getElementById('coinVal').textContent = appState.coins;
document.getElementById('streakVal').textContent = appState.streak;
// 🎵 Switch 卡比慶祝音效 + 卡比親口說「波喲!」(Poyo!)
sound.playCorrectCelebration();
launchFlyingCoins(btnEl);
triggerConfetti(35);
// 卡比歡樂跳躍與對話氣泡
const actor = document.getElementById('kirbyActor');
actor.classList.add('hop-spin');
triggerKirbyChat("波喲!太棒了!💖");
setTimeout(() => actor.classList.remove('hop-spin'), 800);
const cheers = ["答對囉!卡比:波喲~!💖", "算得真快!往前走囉!🐾", "答對啦!往城堡邁進!🌟", "太厲害了!好聰明!🎉"];
notify(cheers[Math.floor(Math.random() * cheers.length)]);
setTimeout(() => {
if (appState.stage < MAX_STAGES) {
appState.stage++;
sound.playKirbyHopSound();
updateKirbyOnTrail(true);
displayQuestion(createMathQuestion());
} else {
showFinalVictory();
}
}, 1100);
} else {
// === 答錯!卡比溫和疑惑萌聲「波?」===
appState.streak = 0;
document.getElementById('streakVal').textContent = appState.streak;
sound.playKirbyDoubt();
triggerKirbyChat("波?再試一次~💡");
const panel = document.getElementById('mathPanel');
panel.classList.remove('shake');
void panel.offsetWidth;
panel.classList.add('shake');
notify("再數數看上面的蘋果喔,加油~💪");
}
}
// 🏆 通關結算 (播放全套卡比勝利之舞!)
function showFinalVictory() {
// 🎵 播放官方經典「卡比勝利之舞」樂曲
sound.playKirbyDanceTheme();
triggerConfetti(180, true);
setTimeout(() => triggerConfetti(120, true), 900);
// 卡比在城堡前歡樂轉圈
const actor = document.getElementById('kirbyActor');
actor.classList.add('hop-spin');
triggerKirbyChat("波喲!勝利通關!🏰🎉");
setTimeout(() => actor.classList.remove('hop-spin'), 1200);
document.getElementById('endCoins').textContent = appState.coins;
document.getElementById('endStreak').textContent = appState.maxStreak;
document.getElementById('victoryModal').classList.add('active');
}
function restartAdventure() {
appState.stage = 1;
appState.coins = 0;
appState.streak = 0;
appState.maxStreak = 0;
appState.isLocked = false;
document.getElementById('coinVal').textContent = '0';
document.getElementById('streakVal').textContent = '0';
document.getElementById('victoryModal').classList.remove('active');
sound.playWarpStarWhoosh();
renderCheckpoints();
displayQuestion(createMathQuestion());
notify("卡比出發囉!波喲~!🏕️");
}
/* =========================================================
7. 頁面啟動與互動事件
========================================================= */
document.addEventListener('DOMContentLoaded', () => {
renderCheckpoints();
displayQuestion(createMathQuestion());
window.addEventListener('resize', () => {
updateKirbyOnTrail(false);
});
document.getElementById('btnQuickReset').addEventListener('click', restartAdventure);
document.getElementById('btnReplay').addEventListener('click', restartAdventure);
const btnSound = document.getElementById('btnSoundToggle');
btnSound.addEventListener('click', () => {
const on = sound.toggleAudio();
btnSound.textContent = on ? '🔊' : '🔇';
notify(on ? '音效已開啟 🔊' : '音效已靜音 🔇');
});
// 💖 點擊頂部「波喲!」按鈕隨時聽卡比發聲
const btnPoyo = document.getElementById('btnPoyoTalk');
btnPoyo.addEventListener('click', () => {
const words = ["波喲!💖", "嗨~!✨", "波喲波喲!🐾"];
const chosen = words[Math.floor(Math.random() * words.length)];
if (Math.random() > 0.5) {
sound.playKirbyPoyo();
} else {
sound.playKirbyHi();
}
triggerKirbyChat(chosen);
});
// 💖 點擊畫面上的卡比本體,卡比會跳躍並說「波喲!」或「嗨~!」
const kirbyActor = document.getElementById('kirbyActor');
kirbyActor.addEventListener('click', () => {
kirbyActor.classList.remove('hop-spin');
void kirbyActor.offsetWidth;
kirbyActor.classList.add('hop-spin');
if (Math.random() > 0.4) {
sound.playKirbyPoyo();
triggerKirbyChat("波喲!(Poyo!) 💖");
} else {
sound.playKirbyHi();
triggerKirbyChat("嗨~!(Hi!) ✨");
}
setTimeout(() => kirbyActor.classList.remove('hop-spin'), 800);
});
// 首次點選喚醒 Web Audio API
document.body.addEventListener('click', () => {
sound.initContext();
}, { once: true });
});
</script>
</body>
</html>