802MathCity / function.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">
<title>能源核心 - The Energy Core</title>
<!-- Google Fonts -->
<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">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
:root {
--glass-bg: rgba(8, 51, 68, 0.85);
/* Dark Cyan */
--glass-border: rgba(6, 182, 212, 0.3);
/* Cyan Border */
}
body {
font-family: 'Noto Sans TC', sans-serif;
background-color: #051015;
/* Slate 900 */
color: white;
overflow: hidden;
/* No scrollbars */
-webkit-user-select: none;
user-select: none;
/* Global Background */
background-image: url('Assets/index/functionbg.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
/* Dark overlay for the whole page */
#global-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
background: rgba(5, 16, 21, 0.6);
/* Semi-transparent Slate 900 */
z-index: -1;
}
/* Canvas as fixed background behind UI but above bg image */
#gameCanvas {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
height: 100dvh;
z-index: 0;
}
/* Removed legacy .dialogue-default and #formula-banner positioning */
/* Layout is now handled by #dialogue-container utility classes in HTML */
#dialogue-box.translate-y-full {
transform: translateY(150%);
}
/* Tech UI Styling */
.glass-panel {
background: var(--glass-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -1px rgba(0, 0, 0, 0.5);
}
.font-tech {
font-family: 'Orbitron', sans-serif;
}
/* Input Styling */
.tech-input {
background: rgba(15, 23, 42, 0.8);
border: 1px solid #475569;
color: #22d3ee;
font-family: 'Orbitron', sans-serif;
transition: all 0.3s ease;
text-align: center;
}
.tech-input:focus {
outline: none;
border-color: #22d3ee;
box-shadow: 0 0 10px rgba(34, 211, 238, 0.3);
}
/* Hide Scrollbar completely */
::-webkit-scrollbar {
display: none;
}
* {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Virtual Keypad */
#virtual-keypad {
position: fixed;
bottom: 20px;
right: 20px;
left: auto;
/* Remove centering */
transform: translateY(120%);
/* Start hidden down */
z-index: 1000;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(34, 211, 238, 0.3);
border-radius: 20px;
padding: 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
/* Dragging */
touch-action: none;
}
#virtual-keypad.active {
transform: translateX(-50%) translateY(0);
}
#virtual-keypad.dragging {
transition: none;
transform: none;
/* Controlled by JS during drag */
}
.keypad-handle {
grid-column: span 3;
height: 20px;
margin-bottom: 5px;
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
cursor: grab;
display: flex;
justify-content: center;
align-items: center;
}
.keypad-handle::after {
content: '';
width: 40px;
height: 4px;
background: rgba(255, 255, 255, 0.4);
border-radius: 2px;
}
.keypad-btn {
width: 60px;
height: 60px;
border-radius: 12px;
background: rgba(30, 41, 59, 0.6);
border: 1px solid rgba(148, 163, 184, 0.2);
color: white;
font-size: 24px;
font-weight: bold;
font-family: 'Orbitron', sans-serif;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.1s;
user-select: none;
}
.keypad-btn:active {
transform: scale(0.95);
background: rgba(34, 211, 238, 0.2);
}
.keypad-btn.action {
background: rgba(15, 23, 42, 0.8);
border-color: rgba(34, 211, 238, 0.4);
color: #22d3ee;
}
.keypad-btn.submit {
background: rgba(6, 182, 212, 0.8);
color: white;
grid-column: span 3;
width: 100%;
height: 50px;
font-size: 18px;
margin-top: 4px;
}
</style>
</head>
<body>
<!-- Global Dark Overlay -->
<div id="global-overlay"></div>
<!-- Background Canvas -->
<canvas id="gameCanvas"></canvas>
<!-- UI HUD -->
<div class="fixed top-4 right-4 z-50">
<a href="index.html"
class="glass-panel rounded-xl px-3 py-2 flex items-center justify-center text-amber-400 hover:bg-slate-800 transition-colors border border-amber-500/30 pointer-events-auto shadow-lg bg-slate-900/80">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</a>
</div>
<!-- Main UI Container -->
<div id="ui-container" class="fixed inset-0 z-10 pointer-events-none">
<!--
Dialogue Container:
- Anchored Vertically Centered Left (Desktop/Tablet)
- Flex Column to stack Banner on top of Box
-->
<div id="dialogue-container"
class="fixed top-1/2 left-4 right-4 md:left-12 md:right-auto md:w-auto -translate-y-1/2 flex flex-col gap-4 items-center md:items-start pointer-events-none z-40">
<!-- Formula Banner -->
<div id="formula-banner" class="hidden pointer-events-auto w-full transition-opacity duration-500">
<div
class="glass-panel px-6 py-4 md:px-8 md:py-5 rounded-3xl border-2 border-cyan-500/30 flex items-center justify-center gap-6 shadow-[0_0_20px_rgba(34,211,238,0.2)]">
<span
class="text-base md:text-xl text-slate-400 font-bold tracking-widest whitespace-nowrap">核心運作法則</span>
<div class="flex items-center gap-1 md:gap-2 text-3xl md:text-4xl font-bold font-tech">
<span class="text-cyan-400">y</span>
<span class="text-slate-500">=</span>
<span class="text-slate-200">2</span>
<span class="text-amber-400">x</span>
<span class="text-slate-500">+</span>
<span class="text-slate-200">3</span>
</div>
</div>
</div>
<!-- Interactive Dialogue Box -->
<div id="dialogue-box"
class="pointer-events-auto w-full glass-panel rounded-3xl p-6 md:p-8 transform transition-all duration-500 translate-y-20 opacity-0 min-h-[200px] flex flex-col justify-center shadow-[0_0_50px_rgba(0,0,0,0.5)] bg-slate-900/90 frame-border">
<!-- Dynamic Content Injected Here -->
</div>
</div>
</div>
<!-- Start Overlay -->
<div id="start-overlay"
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/40 backdrop-blur-sm">
<div class="text-center relative z-10">
<h1
class="text-6xl md:text-8xl font-black text-white mb-2 tracking-widest drop-shadow-[0_0_15px_rgba(34,211,238,0.8)]">
能源核心
</h1>
<h2
class="text-3xl md:text-4xl font-bold font-tech text-cyan-400 mb-8 tracking-[0.5em] uppercase opacity-80">
Energy Core
</h2>
<button onclick="startGame()"
class="group relative px-10 py-4 bg-cyan-600/80 hover:bg-cyan-500 text-white font-bold rounded-xl transition-all shadow-[0_0_20px_rgba(6,182,212,0.4)] text-3xl tracking-widest border border-cyan-400/50 backdrop-blur-sm overflow-hidden">
<span class="relative z-10">進入核心</span>
<div
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:animate-shimmer">
</div>
</button>
</div>
</div>
<!-- Credits -->
<div class="fixed top-2 right-2 text-right text-[10px] text-slate-500 z-50">
<div>Designed by L.Star</div>
</div>
<!-- Virtual Keypad HTML -->
<div id="virtual-keypad" onclick="event.stopPropagation()">
<div class="keypad-handle"></div>
<div class="keypad-btn" onclick="keypad.input(1)">1</div>
<div class="keypad-btn" onclick="keypad.input(2)">2</div>
<div class="keypad-btn" onclick="keypad.input(3)">3</div>
<div class="keypad-btn" onclick="keypad.input(4)">4</div>
<div class="keypad-btn" onclick="keypad.input(5)">5</div>
<div class="keypad-btn" onclick="keypad.input(6)">6</div>
<div class="keypad-btn" onclick="keypad.input(7)">7</div>
<div class="keypad-btn" onclick="keypad.input(8)">8</div>
<div class="keypad-btn" onclick="keypad.input(9)">9</div>
<div class="keypad-btn action" onclick="keypad.input('-')">-</div>
<div class="keypad-btn" onclick="keypad.input(0)">0</div>
<div class="keypad-btn action" onclick="keypad.backspace()"></div>
<div class="keypad-btn submit" onclick="keypad.next()">下一格 / 完成 (ENTER)</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let width, height;
// --- Virtual Keypad System ---
const keypad = {
element: null,
targetInput: null,
init: function () {
this.element = document.getElementById('virtual-keypad');
// Prevent click bubbling from keypad to document (stops accidental closes if logic fails)
this.element.addEventListener('click', (e) => e.stopPropagation());
// Auto-close when clicking outside
document.addEventListener('click', (e) => {
if (this.element.classList.contains('active') &&
!this.element.contains(e.target) &&
e.target !== this.targetInput &&
!e.target.classList.contains('keypad-btn')) {
this.close();
}
});
// --- Drag Logic ---
const handle = this.element.querySelector('.keypad-handle');
this.isDragging = false;
this.startX = 0; this.startY = 0;
this.offsetX = 0; this.offsetY = 0;
const startDrag = (e) => {
// Prevent text selection/scrolling on touch
if (e.type === 'touchstart' && e.cancelable) e.preventDefault();
this.isDragging = true;
this.element.classList.add('dragging');
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
const rect = this.element.getBoundingClientRect();
this.offsetX = clientX - rect.left;
this.offsetY = clientY - rect.top;
// Add listeners to document for smooth dragging outside element
document.addEventListener(e.type.includes('mouse') ? 'mousemove' : 'touchmove', doDrag, { passive: false });
document.addEventListener(e.type.includes('mouse') ? 'mouseup' : 'touchend', endDrag);
};
const doDrag = (e) => {
if (!this.isDragging) return;
e.preventDefault(); // Critical for stopping scroll on mobile
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
// Absolute positioning
this.element.style.transform = 'none';
this.element.style.bottom = 'auto'; // Clear bottom
this.element.style.right = 'auto'; // Clear right
this.element.style.left = (clientX - this.offsetX) + 'px';
this.element.style.top = (clientY - this.offsetY) + 'px';
};
const endDrag = (e) => {
this.isDragging = false;
this.element.classList.remove('dragging');
document.removeEventListener('mousemove', doDrag);
document.removeEventListener('touchmove', doDrag);
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('touchend', endDrag);
};
// Attach start listeners
if (handle) {
handle.addEventListener('mousedown', startDrag);
handle.addEventListener('touchstart', startDrag, { passive: false });
handle.style.cursor = 'grab';
}
},
open: function (inputElement) {
this.targetInput = inputElement;
this.element.classList.add('active');
// Highlight input
document.querySelectorAll('.tech-input').forEach(el => el.classList.remove('border-amber-400', 'ring-2', 'ring-amber-400'));
inputElement.classList.add('border-amber-400', 'ring-2', 'ring-amber-400');
},
close: function () {
this.element.classList.remove('active');
if (this.targetInput) {
this.targetInput.classList.remove('border-amber-400', 'ring-2', 'ring-amber-400');
// Trigger change event if needed
this.targetInput.dispatchEvent(new Event('change'));
this.targetInput = null;
}
},
input: function (val) {
if (!this.targetInput) return;
let currentVal = this.targetInput.value;
if (val === '-') {
if (currentVal.startsWith('-')) this.targetInput.value = currentVal.substring(1);
else this.targetInput.value = '-' + currentVal;
} else {
this.targetInput.value = currentVal + val;
}
},
backspace: function () {
if (!this.targetInput) return;
this.targetInput.value = this.targetInput.value.slice(0, -1);
},
next: function () {
if (!this.targetInput) return;
const currentId = this.targetInput.id;
if (currentId === 'final-a') {
this.open(document.getElementById('final-b'));
return;
}
this.close();
if (currentId === 'ans-input') {
if (currentState === STATE.PHASE2_Q) checkQAnswer();
else if (currentState === STATE.PHASE3_CRISIS) checkFinalAnswer();
else if (currentState === STATE.PHASE4_FIND_B) checkPhase4B();
else if (currentState === STATE.PHASE4_FIND_A) checkPhase4A();
else if (currentState === STATE.PHASE4_VERIFY) checkPhase4Verify();
} else if (currentId === 'test-input') {
runPhase4Test();
} else if (currentId === 'test-input-a') {
runPhase4TestA();
} else if (currentId === 'test-input-verify') {
if (typeof runPhase4VerifyManual === 'function') runPhase4VerifyManual();
} else if (currentId === 'final-b') {
unlockCore();
}
}
};
// Initialize immediately
keypad.init();
document.addEventListener('keydown', (e) => {
if (keypad.element && keypad.element.classList.contains('active') && keypad.targetInput) {
if (e.key >= '0' && e.key <= '9') {
keypad.input(e.key);
} else if (e.key === '-' || e.key === '_') {
keypad.input('-');
} else if (e.key === 'Backspace') {
keypad.backspace();
} else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
keypad.next();
} else if (e.key === 'Escape') {
keypad.close();
}
}
});
// Formula: y = ax + b
let currentA = 2;
let currentB = 3;
const STATE = {
INIT: 0,
STORY_BRIEF: 1,
PHASE1_INTRO: 2,
PHASE2_Q: 3, // General Question State
PHASE2_COORDINATE: 3.5, // New Step: Coordinate Confirmation
PHASE2_PATTERN: 4,
PHASE2_PREDICTION_INTRO: 4.5,
PHASE3_CRISIS: 5,
SUCCESS: 6,
PHASE4_INTRO: 8,
PHASE4_TEST_B: 8.5,
PHASE4_FIND_B: 9,
PHASE4_TEST_A: 9.5,
PHASE4_FIND_A: 10,
PHASE4_VERIFY: 11,
PHASE4_VERIFY_TEST: 11.5,
PHASE5_UNLOCK: 12,
PHASE6_INTRO: 13,
PHASE6_GAME: 14,
PHASE7_SUMMARY: 15,
FAIL: 7
};
const checkPoints = [2, 5, 1, 3, 4]; // The sequence of Xs to check
let collectedPoints = [];
let particles = [];
let graphScale = { xMax: 10, yMax: 20 };
let corePulse = 1.0;
let coreRotation = 0;
// --- Phase 6: Rhythm Game State ---
let rhythmState = {
active: false,
energy: 0, // 0 to 100
score: 0,
combo: 0,
maxCombo: 0,
circles: [], // Array of falling circles
lastSpawnTime: 0,
spawnInterval: 1200, // ms
texts: [] // Floating texts (Perfect/Good)
};
const CORE_RADIUS = 80; // Acceptance radius (Matches visual core size)
// --- State Variables ---
let qStep = 0;
let currentState = STATE.INIT;
// Guide Arrow State
let guideArrowState = {
active: false,
startTime: 0,
targetX: 0,
targetY: 0
};
// --- Audio ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
if (type === 'tick') {
osc.frequency.setValueAtTime(800, now);
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.05);
gain.gain.setValueAtTime(0.05, now);
gain.gain.linearRampToValueAtTime(0, now + 0.05);
osc.start(now); osc.stop(now + 0.05);
} else if (type === 'success') {
osc.type = 'sine';
osc.frequency.setValueAtTime(440, now);
osc.frequency.setValueAtTime(659, now + 0.1); // E
osc.frequency.setValueAtTime(880, now + 0.2); // A
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.5);
osc.start(now); osc.stop(now + 0.5);
} else if (type === 'alarm') {
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.linearRampToValueAtTime(100, now + 0.3);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.3);
osc.start(now); osc.stop(now + 0.3);
} else if (type === 'hit_perfect') {
// High pitch ping
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, now);
osc.frequency.exponentialRampToValueAtTime(800, now + 0.1);
gain.gain.setValueAtTime(0.3, now);
gain.gain.linearRampToValueAtTime(0, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
} else if (type === 'hit_good') {
// Mid pitch
osc.type = 'triangle';
osc.frequency.setValueAtTime(600, now);
gain.gain.setValueAtTime(0.2, now);
gain.gain.linearRampToValueAtTime(0, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
}
}
// --- BGM System ---
let bgmNodes = [];
let isBgmPlaying = false;
function startRhythmBGM() {
if (isBgmPlaying) return;
isBgmPlaying = true;
if (audioCtx.state === 'suspended') audioCtx.resume();
// Simple "Techno" Loop using Oscillator interactions
const baseFreq = 110; // A2
const oscLow = audioCtx.createOscillator();
const oscHigh = audioCtx.createOscillator();
const gainLow = audioCtx.createGain();
const gainHigh = audioCtx.createGain();
oscLow.type = 'square';
oscLow.frequency.value = baseFreq;
oscHigh.type = 'sawtooth';
oscHigh.frequency.value = baseFreq * 2;
// LFO for rhythmic pulsing
const lfo = audioCtx.createOscillator();
lfo.type = 'square';
lfo.frequency.value = 4; // 240 BPM pulses (fast)
const lfoGain = audioCtx.createGain();
lfoGain.gain.value = 0.5;
lfo.connect(lfoGain);
lfoGain.connect(gainLow.gain);
lfoGain.connect(gainHigh.gain);
oscLow.connect(gainLow);
oscHigh.connect(gainHigh);
gainLow.connect(audioCtx.destination);
gainHigh.connect(audioCtx.destination);
// Base volume
gainLow.gain.setValueAtTime(0.1, audioCtx.currentTime);
gainHigh.gain.setValueAtTime(0.05, audioCtx.currentTime);
oscLow.start();
oscHigh.start();
lfo.start();
bgmNodes = [oscLow, oscHigh, lfo];
}
function stopRhythmBGM() {
if (!isBgmPlaying) return;
bgmNodes.forEach(node => {
try { node.stop(); } catch (e) { }
try { node.disconnect(); } catch (e) { }
});
bgmNodes = [];
isBgmPlaying = false;
}
// --- Lifecycle ---
function startGame() {
// Robust Audio Unlock
if (audioCtx.state === 'suspended') {
audioCtx.resume().then(() => {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
gain.gain.value = 0;
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(0);
osc.stop(0.1);
console.log("Audio Context Resumed");
playSound('tick');
});
} else {
playSound('tick');
}
// Try to enter fullscreen
try {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen().catch(e => {
console.log("Fullscreen request failed (likely user denied):", e);
});
} else if (document.documentElement.webkitRequestFullscreen) { /* Safari */
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) { /* IE11 */
document.documentElement.msRequestFullscreen();
}
} catch (err) {
console.log("Fullscreen not supported or allowed");
}
document.getElementById('start-overlay').classList.add('hidden');
resize();
window.addEventListener('resize', resize);
// Input for Rhythm Game
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' && currentState === STATE.PHASE6_GAME) {
handleRhythmInput();
}
});
window.addEventListener('mousedown', (e) => {
if (currentState === STATE.PHASE6_GAME) {
handleRhythmInput();
}
});
window.addEventListener('touchstart', (e) => {
if (currentState === STATE.PHASE6_GAME) {
handleRhythmInput();
e.preventDefault(); // Prevent zoom/scroll
}
}, { passive: false });
loop();
enterPhase(STATE.STORY_BRIEF);
}
function resize() {
const dpr = window.devicePixelRatio || 1;
width = window.innerWidth;
height = window.innerHeight;
// Fix resolution on high-DPI displays
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
// Reset scale before applying new one
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
}
function drawGuideArrows() {
const now = Date.now();
const elapsed = (now - guideArrowState.startTime) / 1000;
const duration = 2.0; // Show for 2 seconds
if (elapsed > duration) {
guideArrowState.active = false;
return;
}
const tx = guideArrowState.targetX;
const ty = guideArrowState.targetY;
const isDesktop = width > 768;
// Convert graph coords to canvas coords
// Logic sync with drawGraph
const padding = 60;
const gw = isDesktop ? width * 0.53 : Math.min(600, width - 40);
const gh = isDesktop ? height * 0.6 : Math.min(400, height * 0.5);
// On Desktop, position graph on the right side (Start at 44% width)
// On Mobile, center it
const gx = isDesktop ? (width * 0.44) : (width - gw) / 2;
const gy = isDesktop ? height * 0.2 : height * 0.1;
const ox = gx + padding;
const oy = gy + gh - padding;
const drawW = gw - padding * 2;
const drawH = gh - padding * 2;
const unitX = drawW / graphScale.xMax;
const unitY = drawH / graphScale.yMax;
const canvasX = ox + tx * unitX;
const canvasY = oy - ty * unitY;
const progress = Math.min(elapsed * 2, 1); // Animate in quickly
ctx.save();
ctx.lineWidth = 4;
ctx.lineCap = 'round';
// Draw X-axis Arrow (Amber, Up)
ctx.strokeStyle = '#F59E0B'; // Amber-500
ctx.fillStyle = '#F59E0B';
ctx.beginPath();
ctx.moveTo(canvasX, oy); // Start from X-axis
ctx.lineTo(canvasX, oy - (oy - canvasY) * progress);
ctx.stroke();
// Draw Arrowhead if fully extended
if (progress > 0.8) {
ctx.beginPath();
ctx.moveTo(canvasX, canvasY + 10);
ctx.lineTo(canvasX - 5, canvasY + 20);
ctx.lineTo(canvasX + 5, canvasY + 20);
ctx.fill();
}
// Draw Y-axis Arrow (Cyan, Right)
ctx.strokeStyle = '#22D3EE'; // Cyan-400
ctx.fillStyle = '#22D3EE';
ctx.beginPath();
ctx.moveTo(ox, canvasY); // Start from Y-axis
ctx.lineTo(ox + (canvasX - ox) * progress, canvasY);
ctx.stroke();
// Draw Arrowhead
if (progress > 0.8) {
ctx.beginPath();
ctx.moveTo(canvasX - 10, canvasY);
ctx.lineTo(canvasX - 20, canvasY - 5);
ctx.lineTo(canvasX - 20, canvasY + 5);
ctx.fill();
}
// Highlight Point
if (progress > 0.9) {
ctx.beginPath();
ctx.arc(canvasX, canvasY, 8, 0, Math.PI * 2);
ctx.fillStyle = '#FFFFFF';
ctx.fill();
ctx.strokeStyle = '#F59E0B';
ctx.stroke();
}
ctx.restore();
}
// --- State Management ---
function enterPhase(phase) {
if (window.keypad) keypad.close(); // Auto-close keypad on any phase change
currentState = phase;
updateUI();
}
function updateUI(errorMsg = null) {
const box = document.getElementById('dialogue-box');
const container = document.getElementById('dialogue-container');
const formulaBanner = document.getElementById('formula-banner');
// Default Container State:
// Desktop: Fixed Left Side (Start 2%, Width 46%). Vertically Centered.
// Reduced gap: Left ends at 40%, Right starts at 48%.
container.className = "fixed z-40 left-4 right-4 top-1/2 -translate-y-1/2 md:translate-y-0 md:top-0 md:bottom-0 md:left-[5%] md:right-auto md:w-[35%] flex flex-col gap-6 justify-center items-center pointer-events-none";
// Default Box State
box.className = "pointer-events-auto w-full glass-panel rounded-3xl p-6 md:p-8 transform transition-all duration-500 min-h-[200px] flex flex-col justify-center shadow-[0_0_50px_rgba(0,0,0,0.5)] bg-slate-900/90 frame-border";
// Specific overrides
if (currentState === STATE.PHASE7_SUMMARY) {
// Summary: Center Screen
container.className = "fixed inset-0 flex items-center justify-center pointer-events-auto z-50";
box.className = "w-[95%] md:w-[800px] glass-panel rounded-3xl p-8 transition-all duration-500 min-h-[400px] flex flex-col justify-start shadow-[0_0_80px_rgba(0,0,0,0.8)] bg-slate-900/95 frame-border relative";
} else if (currentState === STATE.PHASE6_GAME) {
box.classList.add('hidden');
container.classList.add('hidden');
} else if (currentState === STATE.STORY_BRIEF) {
// Story Brief: Remove fixed width, use full container width
// box.classList.add('md:w-[600px]'); // REMOVED
box.classList.remove('hidden');
container.classList.remove('hidden');
} else {
// Normal Phase: Allow full width (controlled by container)
// box.classList.add('md:w-[480px]'); // REMOVED
formulaBanner.className = "hidden pointer-events-auto w-full transition-opacity duration-500";
box.classList.remove('hidden');
container.classList.remove('hidden');
}
box.classList.remove('translate-y-full', 'opacity-0');
let html = '';
switch (currentState) {
case STATE.STORY_BRIEF:
formulaBanner.classList.add('hidden');
html = `
<h2 class="text-4xl font-bold text-amber-400 mb-6">MATH CITY 電力危機</h2>
<p class="text-slate-200 text-3xl leading-relaxed mb-8">
能源核心是整個城市的電力來源,但現在供電系統出了一些問題,需要你來協助修復。<br><br>
但在正式維修前,我們需要先檢測你的運算能力。
</p>
<button onclick="enterPhase(STATE.PHASE1_INTRO)" class="w-full py-5 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl font-bold text-3xl shadow-lg">
開始檢測能力
</button>
`;
break;
case STATE.PHASE1_INTRO:
formulaBanner.classList.remove('hidden');
html = `
<h2 class="text-2xl md:text-3xl font-bold text-cyan-400 mb-4">核心運作法則確認</h2>
<div class="text-center bg-slate-800/80 p-6 rounded-xl border border-cyan-500/30 mb-8">
<div class="font-tech text-2xl md:text-4xl mb-6">
<span class="text-cyan-400">y</span> = 2<span class="text-amber-400">x</span> + 3
</div>
<p class="text-slate-300 text-lg md:text-xl leading-relaxed font-bold">
每投入 <span class="text-amber-400 font-bold text-2xl md:text-4xl mx-2 border-b-2 border-amber-400/50">x</span> 顆晶石,<br>就會產生 <span class="text-cyan-400 font-bold text-2xl md:text-4xl mx-2 border-b-2 border-cyan-400/50">y</span> 點電力。
</p>
</div>
<button onclick="startQPhase()" class="w-full py-5 bg-slate-700 hover:bg-slate-600 text-white rounded-xl font-bold text-xl border border-slate-500">
我準備好了 (NEXT)
</button>
`;
break;
case STATE.PHASE2_Q:
formulaBanner.classList.remove('hidden');
const xVal = checkPoints[qStep];
html = `
<h3 class="text-3xl font-bold text-white mb-2 text-center">能力檢測 (${qStep + 1}/${checkPoints.length})</h3>
<div class="grid grid-cols-2 gap-6 mb-2 mt-4">
<!-- Left: Question -->
<div class="flex flex-col gap-4">
<div class="text-cyan-400 font-bold text-xl border-b border-cyan-500/30 pb-2">1. 算出電力 (y)</div>
<p class="text-slate-300 text-lg">
若投入 <span class="text-amber-400 font-bold text-2xl">${xVal}</span> <span class="text-slate-400 text-base">(<span class="text-amber-400">x=${xVal}</span>)</span> 顆晶石...
</p>
<div class="flex items-center justify-center gap-2 flex-grow">
<span class="font-tech text-3xl text-cyan-400">y = </span>
<div class="relative w-32">
<input id="ans-input" type="text" readonly onclick="keypad.open(this)" class="tech-input w-full p-2 text-3xl rounded-lg" placeholder="?" >
<div class="absolute inset-0 border-2 border-slate-500/50 rounded-lg pointer-events-none"></div>
</div>
</div>
<button onclick="checkQAnswer()" class="w-full h-16 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl font-bold text-xl shadow-lg flex items-center justify-center">
確認電力
</button>
</div>
<!-- Right: Placeholder (Visible but Inactive) -->
<div class="flex flex-col gap-4 opacity-50 grayscale">
<div class="text-slate-400 font-bold text-xl border-b border-slate-500/30 pb-2">2. 坐標描點</div>
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/30 flex flex-col items-center justify-center flex-grow">
<div class="text-sm text-slate-500 mb-1">對應座標</div>
<div class="font-tech text-lg md:text-xl font-bold text-slate-500">
(<span class="text-amber-400">${xVal}</span>, <span class="text-cyan-400">y</span>)
</div>
</div>
<button class="w-full h-16 bg-slate-700 text-slate-500 rounded-xl font-bold text-xl border border-slate-600 cursor-not-allowed flex items-center justify-center">
描繪點 (PLOT)
</button>
</div>
</div>
${errorMsg ? `<div class="text-red-400 text-center font-bold mt-2 border-2 border-red-400 border-dashed animate-pulse bg-slate-900/50 p-2 rounded">${errorMsg}</div>` : ''}
`;
break;
case STATE.PHASE2_COORDINATE:
formulaBanner.classList.remove('hidden');
const xValC = checkPoints[qStep];
const yValC = 2 * xValC + 3;
html = `
<h3 class="text-3xl font-bold text-white mb-2 text-center">能力檢測 (${qStep + 1}/${checkPoints.length})</h3>
<div class="grid grid-cols-2 gap-6 mb-2 mt-4">
<!-- Left: Math Result (Confirmed) -->
<div class="flex flex-col gap-4 opacity-60 grayscale-[0.5] pointer-events-none">
<div class="text-cyan-400 font-bold text-xl border-b border-cyan-500/30 pb-2">1. 算出電力 (y)</div>
<p class="text-slate-300 text-lg">
若投入 <span class="text-amber-400 font-bold text-2xl">${xValC}</span> <span class="text-slate-400 text-base">(<span class="text-amber-400">x=${xValC}</span>)</span> 顆晶石...
</p>
<div class="flex items-center justify-center gap-2 flex-grow">
<span class="font-tech text-3xl text-cyan-400">y = </span>
<div class="relative w-32">
<input type="text" class="tech-input w-full p-2 text-3xl rounded-lg bg-slate-800 text-cyan-400 border-cyan-500" value="${yValC}" disabled>
</div>
</div>
<button class="w-full h-16 bg-slate-700 text-slate-400 rounded-xl font-bold text-xl border border-slate-600 flex items-center justify-center">
已確認
</button>
</div>
<!-- Right: Coordinate (Two States) -->
<div class="flex flex-col gap-4">
<div class="text-amber-400 font-bold text-xl border-b border-amber-500/30 pb-2">2. 坐標描點</div>
<!-- Step 1: Pre-substitution -->
<div id="coord-step-1" class="flex flex-col gap-4 h-full">
<div class="bg-slate-800/80 p-4 rounded-xl border border-cyan-500/50 flex flex-col items-center justify-center flex-grow">
<div class="text-sm text-cyan-300 font-bold mb-2 tracking-widest">對應座標</div>
<div class="font-tech text-2xl md:text-4xl font-bold z-10 mb-2">
(<span class="text-amber-400">${xValC}</span>, <span class="text-cyan-400">y</span>)
</div>
</div>
<button onclick="substituteY()" class="w-full h-16 bg-indigo-600 hover:bg-indigo-500 text-white rounded-xl font-bold text-xl shadow-lg transition-colors flex items-center justify-center">
將 y 代入
</button>
</div>
<!-- Step 2: Post-substitution (Hidden initially) -->
<div id="coord-step-2" class="hidden flex flex-col gap-4 h-full">
<div class="bg-slate-800/80 p-4 rounded-xl border border-cyan-500 shadow-[0_0_20px_rgba(34,211,238,0.2)] flex flex-col items-center justify-center relative overflow-hidden transform transition-all flex-grow animate-pulse-once">
<div class="text-sm text-cyan-300 font-bold mb-2 tracking-widest">對應座標</div>
<div class="font-tech text-2xl md:text-4xl font-bold z-10 mb-2">
(<span class="text-amber-400">${xValC}</span>, <span class="text-cyan-400">${yValC}</span>)
</div>
<div class="absolute inset-0 bg-cyan-500/10 animate-pulse"></div>
</div>
<button onclick="plotCoordinate()" class="w-full h-16 bg-amber-500 hover:bg-amber-400 text-white rounded-xl font-bold text-xl shadow-lg animate-pulse whitespace-nowrap flex items-center justify-center">
描繪點 (PLOT)
</button>
</div>
</div>
</div>
`;
break;
case STATE.PHASE2_PATTERN:
formulaBanner.classList.remove('hidden'); // Ensure banner is visible
html = `
<h3 class="text-3xl font-bold text-white mb-4">規律分析</h3>
<p class="text-slate-300 mb-6">觀察圖表上的這些點,它們呈現什麼樣的排列規律?</p>
<div class="grid grid-cols-3 gap-4">
<button onclick="checkPattern('A')" class="p-4 bg-slate-800 hover:bg-slate-700 rounded-xl border border-slate-600 font-bold text-slate-300 transition-all hover:border-cyan-400 hover:text-white">
雜亂無章
</button>
<button onclick="checkPattern('B')" class="p-4 bg-slate-800 hover:bg-slate-700 rounded-xl border border-slate-600 font-bold text-slate-300 transition-all hover:border-cyan-400 hover:text-white">
圓形分佈
</button>
<button onclick="checkPattern('C')" class="p-4 bg-slate-800 hover:bg-slate-700 rounded-xl border border-slate-600 font-bold text-slate-300 transition-all hover:border-cyan-400 hover:text-white">
一條直線
</button>
</div>
`;
break;
case STATE.PHASE2_PREDICTION_INTRO:
formulaBanner.classList.remove('hidden'); // Keep banner visible
html = `
<h3 class="text-xl font-bold text-green-400 mb-4">分析完成</h3>
<div class="bg-slate-800/80 p-6 rounded-xl border border-green-500/30 mb-6">
<p class="text-slate-200 text-lg leading-relaxed font-bold">
沒錯,所有點都連成一條直線...<br>
<span class="text-cyan-400 block mt-2 text-3xl">也就是我們能預測未來!</span>
</p>
</div>
<button onclick="enterPhase(STATE.PHASE3_CRISIS)" class="w-full py-4 bg-red-600 hover:bg-red-500 text-white rounded-xl font-bold text-xl shadow-lg animate-pulse">
進入緊急狀況演練 (EMERGENCY DRILL)
</button>
`;
break;
case STATE.PHASE3_CRISIS:
formulaBanner.classList.remove('hidden');
html = `
<div class="border-l-4 border-red-500 pl-6">
<h3 class="text-3xl font-bold text-red-500 mb-2 blink-red">狀況:工業用電暴增!</h3>
<p class="text-slate-200 mb-6 text-xl">
因為工業區需求激增,系統現在需要供給 <span class="text-cyan-400 font-bold text-4xl">203</span> 點電力 <span class="text-slate-400 text-base">(<span class="text-cyan-400 font-bold">y=203</span>)</span>。
<br>根據 y=2x+3,請問要投入多少晶石 <span class="text-amber-400 font-bold">(x)</span> 才足夠?
</p>
<div class="flex items-center gap-2 mt-4 bg-slate-800/50 p-4 rounded-xl flex-wrap">
<span class="font-tech text-amber-400 text-4xl whitespace-nowrap">x = </span>
<input id="ans-input" type="text" readonly onclick="keypad.open(this)" class="tech-input flex-1 min-w-[100px] p-2 rounded text-4xl text-amber-400" placeholder="?">
<button onclick="checkFinalAnswer()" class="px-6 py-3 bg-red-600 hover:bg-red-500 text-white rounded font-bold shadow-[0_0_15px_rgba(239,68,68,0.5)] whitespace-nowrap flex-shrink-0">
投入晶石
</button>
</div>
${errorMsg ? `<div class="text-amber-300 text-center font-bold mt-2 text-xl border-2 border-amber-300 border-dashed animate-pulse bg-slate-900/50 p-2 rounded">${errorMsg}</div>` : ''}
</div>
`;
break;
case STATE.SUCCESS:
html = `
<div class="text-center">
<h2 class="text-2xl md:text-2xl font-bold text-green-400 mb-2 font-tech">SYSTEM STABILIZED</h2>
<p class="text-slate-300 mb-6 text-xl">預測成功!核心運作恢復正常。</p>
<!-- Teaching Content -->
<div class="bg-slate-800/80 p-4 rounded-xl border border-cyan-500/30 mb-6 text-left">
<h3 class="text-lg md:text-2xl font-bold text-white mb-2 text-center">任務小結:什麼是「函數」?</h3>
<p class="text-slate-200 text-base md:text-xl leading-relaxed mb-3">
剛剛我們使用的「能量轉換法則」,在數學上就叫做<span class="text-amber-400 font-bold text-xl md:text-2xl mx-1">「函數」(Function)</span>。
</p>
<p class="text-slate-300 text-base md:text-xl leading-relaxed">
函數就像一座<span class="text-cyan-400 font-bold">工廠</span>:<br>
它會把原料 <span class="font-bold text-amber-400">x (晶石)</span>,藉由固定的規則,<br>轉換成產品 <span class="font-bold text-cyan-400">y (電力)</span>。
</p>
</div>
<div class="flex flex-col gap-3 max-w-md mx-auto">
<a href="index.html" class="block w-full py-3 bg-slate-600 hover:bg-slate-500 text-white rounded-xl font-bold text-lg transition-all border border-slate-500 hover:border-white">
回到 Math City
</a>
<button onclick="enterPhase(STATE.PHASE4_INTRO)" class="relative overflow-hidden w-full py-3 bg-red-600 hover:bg-red-500 text-white rounded-xl font-bold text-lg shadow-lg group border border-red-400">
<span class="relative z-10 flex items-center justify-center gap-2">
⚠️ 修復電力系統 (進階挑戰)
</span>
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover:animate-shimmer"></div>
</button>
</div>
</div>
`;
break;
case STATE.PHASE4_INTRO:
formulaBanner.classList.remove('hidden'); // Ensure banner is visible
html = `
<div class="border-l-4 border-amber-500 pl-6">
<h3 class="text-3xl font-bold text-amber-500 mb-2 blink-red">警告:公式異變!</h3>
<p class="text-slate-200 mb-6 text-xl">
檢測到晶石純度下降,能量轉換公式已改變!<br>
系統無法自動運作,你必須重新推導公式。
</p>
<div class="bg-slate-800/50 p-4 rounded-xl mb-4 border border-slate-600">
<span class="font-tech text-4xl block text-center tracking-widest">
y = <span class="text-amber-400 animate-pulse">?</span>x + <span class="text-cyan-400 animate-pulse">?</span>
</span>
</div>
<button onclick="startPhase4Deduction()" class="w-full py-3 bg-amber-600 hover:bg-amber-500 text-white rounded-xl font-bold shadow-lg">
開始推導 (Start Deduction)
</button>
</div>
`;
break;
case STATE.PHASE4_TEST_B:
formulaBanner.classList.remove('hidden'); // Ensure banner is visible
html = `
<h3 class="text-3xl font-bold text-white mb-2">步驟 1:數據校準 b</h3>
<p class="text-slate-300 mb-4">
你需要測試不同的晶石投入量,來推導出新的公式。
<br><span class="text-base text-slate-400">提示:什麼時候 <span class="text-red-400">a</span> 會消失不見? (試試投入 0 顆)</span>
</p>
<div class="flex items-center gap-2 mt-4 bg-slate-800/50 p-4 rounded-xl justify-center flex-wrap">
<span class="font-tech text-amber-400 text-3xl whitespace-nowrap">投入 x = </span>
<input id="test-input" type="text" readonly onclick="keypad.open(this)" class="tech-input w-24 p-2 rounded text-3xl text-amber-400 text-center" placeholder="0" value="0">
<button onclick="runPhase4Test()" class="px-6 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded font-bold shadow-lg whitespace-nowrap">
啟動運作
</button>
${errorMsg ? `<div class="text-amber-400 text-center font-bold mt-2 text-xl border-2 border-amber-400 border-dashed animate-pulse p-2 rounded">${errorMsg}</div>` : ''}
</div>
`;
break;
case STATE.PHASE4_FIND_B:
formulaBanner.classList.remove('hidden');
html = `
<h3 class="text-3xl font-bold text-white mb-2">步驟 1:尋找初始值 (b)</h3>
<p class="text-slate-300 mb-4 text-2xl">
當投入 <span class="text-amber-400 font-bold">0</span> 顆時,
電力顯示為 <span class="text-cyan-400 font-bold">5</span>。
<br><span class="font-tech text-3xl block mt-2"><span class="text-cyan-400">5</span> = a × <span class="text-amber-400">0</span> + b</span>
<span class="font-tech text-3xl block"><span class="text-cyan-400">5</span> = b</span>
</p>
<div class="flex items-center justify-center gap-2 text-3xl font-tech mb-6 bg-slate-800 p-4 rounded-lg">
<span>b = </span>
<input id="ans-input" type="text" readonly onclick="keypad.open(this)" class="w-24 bg-slate-700 text-cyan-400 text-center rounded border border-slate-500 focus:border-cyan-400 outline-none p-1" placeholder="?">
</div>
${errorMsg ? `<div class="text-red-400 text-center font-bold mb-4 border-2 border-red-400 border-dashed animate-pulse bg-slate-900/50 p-2 rounded">${errorMsg}</div>` : ''}
<button onclick="checkPhase4B()" class="w-full py-3 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl font-bold">
確認校準 Check
</button>
`;
break;
case STATE.PHASE4_TEST_A:
formulaBanner.classList.remove('hidden');
html = `
<h3 class="text-3xl font-bold text-white mb-2">步驟 2:數據校準 a</h3>
<p class="text-slate-300 mb-4">
現在已知 b=5。接下來需要找出變化率 a。
<br><span class="text-base text-slate-400">提示:試試看投入 1 顆晶石?</span>
</p>
<div class="flex items-center gap-2 mt-4 bg-slate-800/50 p-4 rounded-xl justify-center flex-wrap">
<span class="font-tech text-amber-400 text-3xl whitespace-nowrap">投入 x = </span>
<input id="test-input-a" type="text" readonly onclick="keypad.open(this)" class="tech-input w-24 p-2 rounded text-3xl text-amber-400 text-center" placeholder="1" value="1">
<button onclick="runPhase4TestA()" class="px-6 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded font-bold shadow-lg whitespace-nowrap">
啟動運作
</button>
</div>
${errorMsg ? `<div class="text-amber-400 text-center font-bold mt-2 text-xl border-2 border-amber-400 border-dashed animate-pulse p-2 rounded">${errorMsg}</div>` : ''}
`;
break;
case STATE.PHASE4_FIND_A:
formulaBanner.classList.remove('hidden');
html = `
<h3 class="text-3xl font-bold text-white mb-2">步驟 2:解出 a (Solve for a)</h3>
<p class="text-slate-300 mb-4">
測試投入 <span class="text-amber-400 font-bold">${lastTestAX}</span> 顆晶石,
電力變為 <span class="text-cyan-400 font-bold">${lastTestAY}</span>。
</p>
<div class="bg-slate-800/50 p-4 rounded-xl mb-4 text-center border border-slate-700">
<span class="text-3xl font-tech block mt-2 text-white"><span class="text-cyan-400">${lastTestAY}</span> = a <span class="text-amber-400">× ${lastTestAX}</span> + 5</span>
</div>
<p class="text-slate-300 mb-4 text-center">請問 <span class="text-amber-400 font-bold">a</span> 是多少?</p>
<div class="flex flex-wrap gap-2 mb-2 items-center justify-center text-3xl font-tech text-amber-400">
<span>a =</span>
<input id="ans-input" type="text" readonly onclick="keypad.open(this)" class="tech-input w-24 p-2 rounded text-3xl text-center text-amber-400" placeholder="?">
<button onclick="checkPhase4A()" class="w-full md:w-auto px-8 py-2 bg-amber-600 hover:bg-amber-500 text-white rounded-xl font-bold shadow-lg transition-transform hover:scale-105 whitespace-nowrap text-lg font-sans">確認</button>
</div>
${errorMsg ? `<div class="text-red-400 text-center font-bold mb-2 border-2 border-red-400 border-dashed animate-pulse bg-slate-900/50 p-2 rounded">${errorMsg}</div>` : ''}
`;
break;
case STATE.PHASE4_VERIFY:
formulaBanner.classList.remove('hidden');
html = `
<h3 class="text-3xl font-bold text-white mb-2">步驟 3:最終驗算</h3>
<p class="text-slate-300 mb-4">
假設公式為 <span class="font-bold text-white">y = 3x + 5</span>。
<br>若投入 <span class="text-amber-400 font-bold">10</span> 顆晶石,電力應為多少?
</p>
<div class="flex flex-wrap gap-2 mt-6 items-center justify-center text-3xl font-tech text-white">
<span>y =</span>
<input id="ans-input" type="text" readonly onclick="keypad.open(this)" class="tech-input w-24 p-2 rounded text-3xl text-center text-white" placeholder="?">
<button onclick="checkPhase4Verify()" class="w-full md:w-auto px-8 py-2 bg-green-600 hover:bg-green-500 text-white rounded-xl font-bold shadow-lg transition-transform hover:scale-105 whitespace-nowrap text-lg font-sans">驗證</button>
</div>
${errorMsg ? `<div class="text-red-400 text-center font-bold mt-4 border-2 border-red-400 border-dashed animate-pulse bg-slate-900/50 p-2 rounded">${errorMsg}</div>` : ''}
`;
break;
case STATE.PHASE5_UNLOCK:
formulaBanner.classList.remove('hidden'); // Ensure banner is visible
html = `
<div class="text-center w-full">
<h3 class="text-3xl font-bold text-amber-400 mb-6 drop-shadow-lg">輸入核心密碼 (Core Override)</h3>
<div class="flex items-center justify-center gap-2 text-4xl md:text-4xl font-tech font-bold mb-8 bg-slate-900/80 p-6 rounded-3xl border-2 border-slate-700 w-full max-w-lg mx-auto">
<span class="text-cyan-400">y</span>
<span class="text-slate-500">=</span>
<input id="final-a" type="text" readonly onclick="keypad.open(this)" class="w-20 bg-slate-800 text-amber-400 text-center rounded border border-slate-600 focus:border-amber-400 outline-none p-2" placeholder="?">
<span class="text-amber-400">x</span>
<span class="text-slate-500">+</span>
<input id="final-b" type="text" readonly onclick="keypad.open(this)" class="w-20 bg-slate-800 text-cyan-400 text-center rounded border border-slate-600 focus:border-cyan-400 outline-none p-2" placeholder="?">
</div>
<button onclick="unlockCore()" class="w-full py-4 bg-gradient-to-r from-amber-600 to-red-600 text-white rounded-xl font-bold text-xl shadow-lg hover:shadow-amber-500/50 transition-all border border-amber-500/30">
解鎖大門 (UNLOCK)
</button>
${errorMsg ? `<div class="text-red-400 text-center font-bold mt-4 border-2 border-red-400 border-dashed animate-pulse bg-slate-900/50 p-2 rounded text-xl">${errorMsg}</div>` : ''}
</div>
`;
break;
case STATE.PHASE6_INTRO:
formulaBanner.classList.add('hidden');
html = `
<h2 class="text-3xl font-bold text-cyan-400 mb-4">核心同步程序 (Core Synchronization)</h2>
<p class="text-slate-200 text-xl leading-relaxed mb-6">
核心已解鎖,但能量極不穩定!<br>
我們需要透過<span class="text-amber-400 font-bold">手動脈衝</span>來穩定核心頻率。
</p>
<div class="bg-slate-800/80 p-5 rounded-xl border border-cyan-500/50 mb-6 text-center shadow-[0_0_15px_rgba(6,182,212,0.2)]">
<p class="text-amber-400 text-xl font-bold mb-3 border-b border-cyan-500/30 pb-2">🕹️ 脈衝守護操作說明</p>
<p class="text-lg text-slate-200 leading-relaxed">
外圍會有<span class="text-cyan-400 font-bold">彩色光圈</span>向內縮小,<br>
請在光圈<span class="text-amber-400 font-bold bg-amber-900/40 px-2 py-1 rounded">正好重疊</span>到<span class="text-white font-bold">中心白色圓環</span>的瞬間,<br>
<span class="text-green-400 font-bold">點擊螢幕任意處</span> 或按下 <span class="text-green-400 font-bold px-2 py-1 bg-slate-700 rounded border border-slate-500">空白鍵</span>!
</p>
</div>
<button onclick="startRhythmGame()" class="w-full py-4 bg-cyan-600 hover:bg-cyan-500 text-white rounded-xl font-bold text-xl shadow-lg animate-pulse">
開始同步 (START)
</button>
`;
break;
case STATE.PHASE6_GAME:
// Hide dialogue box during game, only show overlay info via Canvas or minimal DOM
box.classList.add('opacity-0', 'translate-y-20');
formulaBanner.classList.add('hidden');
// We will render UI on canvas or absolute overlay
break;
case STATE.PHASE7_SUMMARY:
// Break out of the container layout for Full Screen Center
// We modify the box styling directly to be fixed center
box.className = "fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-[95%] md:w-[70%] glass-panel rounded-3xl p-8 transition-all duration-500 min-h-[400px] max-h-[90vh] overflow-y-auto flex flex-col justify-start shadow-[0_0_80px_rgba(0,0,0,0.8)] bg-slate-900/95 frame-border z-50 pointer-events-auto";
const currentScore = rhythmState.score;
const highScore = localStorage.getItem('math_city_score_function') || 0;
html = `
<div class="text-center w-full h-full flex flex-col items-center">
<h2 class="text-xl md:text-2xl font-black text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-green-400 mb-6 drop-shadow-[0_0_15px_rgba(34,211,238,0.6)] tracking-widest">
任務完成 SYSTEM RESTORED
</h2>
<div class="text-amber-400 font-tech text-base md:text-2xl mb-10 tracking-[0.5em]">MISSION ACCOMPLISHED</div>
<!-- Score Board -->
<div class="flex gap-16 mb-10 bg-slate-800/80 px-16 py-8 rounded-3xl border-2 border-slate-600 transform scale-110">
<div class="text-center">
<div class="text-xl text-slate-400 uppercase tracking-widest mb-2">本次得分</div>
<div class="text-4xl font-black text-cyan-400 font-tech shimmer">${currentScore}</div>
</div>
<div class="w-px bg-slate-500"></div>
<div class="text-center">
<div class="text-xl text-slate-400 uppercase tracking-widest mb-2">歷史最高</div>
<div class="text-4xl font-black text-amber-400 font-tech">${highScore}</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 w-full text-left mb-12 max-w-6xl">
<!-- Insight 1 -->
<div class="bg-slate-800/50 p-10 rounded-3xl border border-cyan-500/20 hover:border-cyan-500/50 transition-colors">
<h3 class="flex items-center gap-4 text-4xl font-bold text-cyan-400 mb-6">
<span class="text-5xl">🔮</span> 預測未來 (Prediction)
</h3>
<p class="text-slate-300 text-3xl leading-relaxed mb-4">
「當我們知道規則 <span class="bg-slate-900 px-3 rounded text-cyan-300 font-tech">y=ax+b</span> 時,就能精準預測未來的結果。」
</p>
<div class="text-xl text-slate-500 border-t border-slate-700 pt-4">
回顧:就像你在階段三,算出需要投入多少晶石才能救急。
</div>
</div>
<!-- Insight 2 -->
<div class="bg-slate-800/50 p-10 rounded-3xl border border-amber-500/20 hover:border-amber-500/50 transition-colors">
<h3 class="flex items-center gap-4 text-4xl font-bold text-amber-400 mb-6">
<span class="text-5xl">📐</span> 數學建模 (Modeling)
</h3>
<p class="text-slate-300 text-3xl leading-relaxed mb-4">
「當規則改變或未知時,我們也能透過觀察數據的變化,反推回公式本身。」
</p>
<div class="text-xl text-slate-500 border-t border-slate-700 pt-4">
回顧:就像你在階段四,透過測試 x=0 和 x=1,重新找回消失的 a 與 b。
</div>
</div>
</div>
<p class="text-slate-400 italic mb-12 text-2xl max-w-3xl whitespace-nowrap">
「這就是科學家與工程師的超能力:觀察數據 <span class="text-white">→</span> 找出規則 <span class="text-white">→</span> 預測未來。」
</p>
<a href="index.html" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-6 px-20 rounded-full text-4xl shadow-[0_0_40px_rgba(79,70,229,0.6)] transition-all transform hover:scale-105 hover:rotate-1">
回到 Math City
</a>
</div>
`;
break;
case STATE.PHASE4_VERIFY_TEST:
html = `
<h3 class="text-3xl font-bold text-amber-400 mb-2 font-tech tracking-widest border-b border-amber-500/30 pb-2">
<span class="mr-2">⚡</span> 最終驗證 FINAL VERIFICATION
</h3>
<p class="text-slate-300 leading-relaxed mb-6">
公式推導完成:<span class="text-white font-bold">y = 3x + 5</span>
<br>現在,請實際投入 <span class="text-amber-400">10</span> 顆晶石來驗證預測是否準確。
</p>
<div class="flex items-center gap-4 mb-2">
<span class="text-3xl font-tech text-amber-500">x =</span>
<div class="relative flex-1">
<input type="text" readonly onclick="keypad.open(this)" id="test-input-verify" class="tech-input w-full p-4 rounded-xl text-3xl font-bold" placeholder="輸入 10">
<div class="absolute right-4 top-1/2 -translate-y-1/2 text-slate-500 text-sm">晶石</div>
</div>
</div>
<button onclick="runPhase4VerifyManual()" class="w-full mt-4 py-3 bg-cyan-600/80 hover:bg-cyan-500 text-white font-bold rounded-xl transition-all shadow-[0_0_15px_rgba(6,182,212,0.3)]">
啟動測試 / VERIFY
</button>
`;
break;
}
box.innerHTML = html;
// Focus input if available
const input = box.querySelector('input');
if (input) input.focus();
}
// --- Logic Operations ---
function startQPhase() {
if (window.keypad) keypad.close();
qStep = 0;
enterPhase(STATE.PHASE2_Q);
}
function checkQAnswer() {
const input = document.getElementById('ans-input');
const val = parseInt(input.value);
const targetX = checkPoints[qStep];
const correctY = 2 * targetX + 3;
if (isNaN(val)) return;
if (val === 222) { // Cheat code
playSound('success');
// Skip everything -> Success
addPoint(100, 203); // fake the final point
enterPhase(STATE.SUCCESS);
return;
}
if (val === correctY) {
playSound('success');
// Old: addPoint(targetX, correctY);
// New: Go to Coordinate Confirmation Step
enterPhase(STATE.PHASE2_COORDINATE);
} else {
playSound('alarm');
input.classList.add('animate-shake');
setTimeout(() => input.classList.remove('animate-shake'), 500);
// Show hint
updateUI(`提示: 2 × ${targetX} + 3 = ?`);
}
}
function substituteY() {
playSound('swish'); // Optional sound
document.getElementById('coord-step-1').classList.add('hidden');
const step2 = document.getElementById('coord-step-2');
step2.classList.remove('hidden');
// Add a small pop animation or effect if desired
}
function plotCoordinate() {
const targetX = checkPoints[qStep];
const correctY = 2 * targetX + 3;
addPoint(targetX, correctY);
// Trigger Animation
guideArrowState.active = true;
guideArrowState.startTime = Date.now();
guideArrowState.targetX = targetX;
guideArrowState.targetY = correctY;
playSound('tick');
qStep++;
if (qStep < checkPoints.length) {
// Next question
enterPhase(STATE.PHASE2_Q);
} else {
// All questions done
setTimeout(() => enterPhase(STATE.PHASE2_PATTERN), 500);
}
}
function checkFinalAnswer() {
const input = document.getElementById('ans-input');
const val = parseInt(input.value);
if (val === 100 || val === 222) { // 222 is cheat code
playSound('success');
graphScale.xMax = 110;
graphScale.yMax = 220;
addPoint(100, 203);
enterPhase(STATE.SUCCESS);
} else {
playSound('alarm');
input.classList.add('animate-shake');
setTimeout(() => input.classList.remove('animate-shake'), 500);
input.value = '';
// Show hint: 203 - 3 = 200, so 2x = 200
updateUI("提示: 203 = 2x + 3");
}
}
function checkPattern(choice) {
if (choice === 'C') {
playSound('success');
enterPhase(STATE.PHASE2_PREDICTION_INTRO);
} else {
alert("再仔細看看...?");
}
}
function startPhase4Deduction() {
// Reset graph for new formula (Scale reduced for visibility)
collectedPoints = [];
currentA = 3;
currentB = 5;
// Adjust scale for Phase 4 (Close up view)
graphScale.xMax = 15;
graphScale.yMax = 40;
// Do NOT add point yet. Wait for manual test.
enterPhase(STATE.PHASE4_TEST_B);
// Update Banner to y=ax+b style
document.querySelector('#formula-banner .font-tech').innerHTML =
`<span class="text-cyan-400">y</span><span class="text-slate-500">=</span><span class="text-red-500 animate-pulse">a</span><span class="text-amber-400">x</span><span class="text-slate-500">+</span><span class="text-red-500 animate-pulse">b</span>`;
}
function runPhase4Test() {
const input = document.getElementById('test-input');
const x = parseFloat(input.value);
if (isNaN(x)) return;
if (x === 0) {
const y = 3 * x + 5;
addPoint(x, y);
playSound('tick');
setTimeout(() => {
enterPhase(STATE.PHASE4_FIND_B);
}, 1000);
} else {
playSound('alarm');
const y = 3 * x + 5;
// Don't plot point to avoid confusing them? Or plot it but don't proceed?
// User said "don't plot point... show hint".
// Hint display logic
const hintY = 14;
// Wait, if x=3, y=14. Formula y=ax+b becomes 14 = 3a + b.
updateUI(`錯誤!若投入 ${x} 顆,電力為 ${y}。<br>公式變為 <span class="text-amber-400 font-tech">${y} = ${x}a + b</span>。<br>a 沒有消失,無法算出 b。只有什麼數字才能讓 a 消失?`);
}
}
function checkPhase4B() {
const val = parseInt(document.getElementById('ans-input').value);
if (val === 5) {
playSound('success');
// Update Banner: y = ?x + 5
// User requested: 5 is white (slate-200), x amber, a blinking red
document.querySelector('#formula-banner .font-tech').innerHTML =
`<span class="text-cyan-400">y</span><span class="text-slate-500">=</span><span class="text-red-500 animate-pulse">a</span><span class="text-amber-400">x</span><span class="text-slate-500">+</span><span class="text-slate-200">5</span>`;
// Go to Step 2 Manual Test
enterPhase(STATE.PHASE4_TEST_A);
} else {
playSound('alarm');
updateUI("錯誤!當 x=0 時,y=5。公式 y = ax + b 變成 5 = a(0) + b,所以 b 是...?");
}
}
// Global var to store X logic for Phase 4 Step 2
let lastTestAX = 1;
let lastTestAY = 8;
function runPhase4TestA() {
const input = document.getElementById('test-input-a');
const x = parseInt(input.value);
if (isNaN(x) || x < 1 || x > 9) {
playSound('alarm');
updateUI("為了方便觀察,請輸入 1 到 9 之間的整數。");
input.value = '';
return;
}
const y = 3 * x + 5;
addPoint(x, y);
playSound('tick');
// Store for next step text
lastTestAX = x;
lastTestAY = y;
setTimeout(() => {
enterPhase(STATE.PHASE4_FIND_A);
}, 1000);
}
function checkPhase4A() {
const val = parseInt(document.getElementById('ans-input').value);
if (val === 3) {
playSound('success');
// Update Banner: y = 3x + 5
document.querySelector('#formula-banner .font-tech').innerHTML =
`<span class="text-cyan-400">y</span><span class="text-slate-500">=</span><span class="text-slate-200">3</span><span class="text-amber-400">x</span><span class="text-slate-500">+</span><span class="text-slate-200">5</span>`;
enterPhase(STATE.PHASE4_VERIFY);
} else {
playSound('alarm');
updateUI(`錯誤,${lastTestAY} = ${lastTestAX}a + 5 則 a = ?`);
}
}
function checkPhase4Verify() {
const val = parseInt(document.getElementById('ans-input').value);
if (val === 35) {
playSound('success');
// Auto verification visual
if (window.keypad) keypad.close();
// Add point without particles, but with blink
addPoint(10, 35, false, true);
updateUI("預測正確!投入 10 顆晶石,電力確實為 35!<br><span class='text-amber-400 text-base'>(確認點連成一線...)</span>");
// RESTORE INPUT VALUE because updateUI cleared it
setTimeout(() => {
const input = document.getElementById('ans-input');
if (input) input.value = "35";
}, 50);
// Disable button
const btn = document.querySelector('#dialogue-box button');
if (btn) {
btn.disabled = true;
btn.classList.add('opacity-50', 'cursor-not-allowed', 'pointer-events-none');
btn.innerHTML = '驗證成功 Verified';
}
// Add guide text to look at the graph
const inputContainer = document.getElementById('ans-input')?.parentElement?.parentElement; // Adjusted selector
if (inputContainer) {
const guide = document.createElement('div');
guide.className = "text-center text-green-400 font-bold mt-2 text-xl animate-pulse";
guide.innerHTML = "請觀察圖表上的點 (10,35)...";
inputContainer.insertAdjacentElement('afterend', guide);
}
// Delay for user to see the result matches the line before unlocking
// Extended delay to 3.5s to ensure they see the point on the line
// Delay for user to see the result matches the line before unlocking
// Extended delay to 3.5s to ensure they see the point on the line
setTimeout(() => {
const guide = document.querySelector('.text-green-400.animate-pulse');
if (guide) guide.remove();
enterPhase(STATE.PHASE5_UNLOCK);
}, 3500);
} else {
playSound('alarm');
// Use on-screen hint instead of alert
updateUI("驗算失敗!請檢查公式: y = 3×10 + 5 = ?");
}
}
function unlockCore() {
const a = parseInt(document.getElementById('final-a').value);
const b = parseInt(document.getElementById('final-b').value);
if (a === 3 && b === 5) {
if (window.keypad) keypad.close();
playSound('success');
// Instead of alert, go to Phase 6
enterPhase(STATE.PHASE6_INTRO);
} else {
playSound('alarm');
const box = document.querySelector('.bg-slate-900\\/80'); // Target container
if (box) box.classList.add('animate-shake');
setTimeout(() => box && box.classList.remove('animate-shake'), 500);
// Show hint on screen (simulated by updateUI call inside loop, but here distinct)
// Just let user retry, maybe add text
// Since updateUI overwrites everything, we just re-render PHASE5_UNLOCK with Error?
// But unlockCore is called from button. We can append error.
const inputs = document.querySelectorAll('#final-a, #final-b');
inputs.forEach(i => i.value = '');
updateUI("密碼錯誤!提示:y = 3x + 5 (a=3, b=5)");
}
}
// --- Phase 6: Rhythm Game Logic ---
function startRhythmGame() {
rhythmState.energy = 0;
rhythmState.score = 0;
rhythmState.combo = 0;
rhythmState.circles = [];
rhythmState.active = true;
// startRhythmBGM(); // Disabled by user request
enterPhase(STATE.PHASE6_GAME);
}
function updateRhythm(dt) {
if (!rhythmState.active) return;
const now = Date.now();
// Spawn circles
if (now - rhythmState.lastSpawnTime > rhythmState.spawnInterval) {
spawnRhythmCircle();
rhythmState.lastSpawnTime = now;
// Speed up slightly as energy grows
rhythmState.spawnInterval = Math.max(600, 1200 - (rhythmState.energy * 5));
}
// Update circles
// Radius shrinks: starts at say 300, target 60 (CORE_RADIUS)
// Speed depends on spawnInterval roughly
for (let i = rhythmState.circles.length - 1; i >= 0; i--) {
const c = rhythmState.circles[i];
const speed = 150 * dt; // pixels per second-ish
c.r -= speed;
// Miss condition
if (c.r < CORE_RADIUS - 20) {
// Miss
rhythmState.circles.splice(i, 1);
handleMiss();
}
}
// Check win
if (rhythmState.energy >= 100) {
rhythmState.active = false;
stopRhythmBGM(); // Stop music
playSound('success'); // Big success sound later
// Save High Score
const currentBest = parseInt(localStorage.getItem('math_city_score_function') || '0');
if (rhythmState.score > currentBest) {
localStorage.setItem('math_city_score_function', rhythmState.score.toString());
}
setTimeout(() => enterPhase(STATE.PHASE7_SUMMARY), 1500); // 1.5s delay for victory effect
}
}
function drawRhythmBackground() {
// Cyber Tunnel Effect
const cx = width / 2;
const cy = height / 2;
const time = Date.now() / 1000;
// Clear with dark blue tint
ctx.fillStyle = '#051015';
ctx.fillRect(0, 0, width, height);
// Rotating Grid
ctx.save();
ctx.translate(cx, cy);
// Slowly rotate the whole world
ctx.rotate(time * 0.1);
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(6, 182, 212, 0.15)'; // Faint Cyan
// Radial lines
const rays = 12;
for (let i = 0; i < rays; i++) {
const angle = (i / rays) * Math.PI * 2;
ctx.beginPath();
ctx.moveTo(0, 0);
// Draw far out to cover screen corners
ctx.lineTo(Math.cos(angle) * width, Math.sin(angle) * width);
ctx.stroke();
}
// Expanding Concentric Hexagons (Tunnel)
const speed = 100; // pixels per second expansion
const spacing = 200;
const offset = (Date.now() % 2000) / 2000 * spacing; // 0 to 200
for (let r = offset; r < width; r += spacing) {
if (r < 10) continue; // too small
const alpha = Math.min(1, r / 300); // Fade in from center
ctx.strokeStyle = `rgba(6, 182, 212, ${alpha * 0.3})`;
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const a = i * Math.PI / 3;
const x = r * Math.cos(a);
const y = r * Math.sin(a);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
}
ctx.restore();
}
function spawnRhythmCircle() {
rhythmState.circles.push({
r: 400, // Start radius (outside screen mostly)
color: Math.random() > 0.5 ? '#22d3ee' : '#f59e0b' // Cyan or Amber
});
}
function handleRhythmInput() {
// Check the smallest circle
if (rhythmState.circles.length === 0) return;
// Sort by radius, usually the first one (index 0) is smallest due to spawn order
// But let's find the one closest to CORE_RADIUS
let targetIndex = -1;
let minDiff = 999;
rhythmState.circles.forEach((c, index) => {
const diff = Math.abs(c.r - CORE_RADIUS);
if (diff < minDiff) {
minDiff = diff;
targetIndex = index;
}
});
if (targetIndex !== -1) {
const diff = minDiff;
const tolerance = 30; // pixels tolerance
if (diff < 10) {
// Perfect
handleHit('PERFECT', targetIndex);
} else if (diff < tolerance) {
// Good
handleHit('GOOD', targetIndex);
} else {
// Too early/late logic if we want, or just ignore if it's way off
// If it's somewhat close but not hit, maybe miss?
// Let's just ignore clicks that are way off so we don't punish accidental clicks too much
}
}
}
function handleHit(type, index) {
rhythmState.circles.splice(index, 1);
if (type === 'PERFECT') {
rhythmState.energy = Math.min(100, rhythmState.energy + 10);
// Combo Bonus: +30 per combo
const bonus = rhythmState.combo * 30;
rhythmState.score += (500 + bonus);
rhythmState.combo++;
spawnFloatingText(`PERFECT! +${500 + bonus}`, '#fbbf24'); // Gold
playSound('hit_perfect');
playSound('hit_perfect');
} else {
// Fix: Good also gives 10 energy to prevent "extending game" exploit
rhythmState.energy = Math.min(100, rhythmState.energy + 10);
// Combo Bonus: +10 per combo
const bonus = rhythmState.combo * 10;
rhythmState.score += (50 + bonus);
rhythmState.combo++;
spawnFloatingText(`GOOD +${50 + bonus}`, '#22d3ee'); // Blue
playSound('hit_good');
}
// Core pulse
corePulse = 1.5;
}
function handleMiss() {
rhythmState.energy = Math.max(0, rhythmState.energy - 2);
rhythmState.combo = 0;
// Penalize score to prevent farming (losing energy to re-gain points)
// Deduction = Potential gain of a Perfect Hit (500)
rhythmState.score = Math.max(0, rhythmState.score - 500);
spawnFloatingText("MISS", '#ef4444');
playSound('alarm');
}
function spawnFloatingText(text, color) {
rhythmState.texts.push({
text: text,
x: width / 2,
y: height / 2 - 80,
life: 1.0,
color: color
});
}
function addPoint(x, y, spawnParticles = true, blink = false) {
if (!collectedPoints.find(p => p.x === x)) {
collectedPoints.push({ x, y, blink });
// Spawn particle effect
if (spawnParticles) {
for (let i = 0; i < 10; i++) {
particles.push({
x: width / 2,
y: height / 2,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
life: 1.0,
color: '#fbbf24'
});
}
}
}
}
// --- Canvas Rendering (Same as before but simplified) ---
function loop() {
ctx.clearRect(0, 0, width, height); // Clear for transparency
// Special update for Rhythm Game
if (currentState === STATE.PHASE6_GAME) {
updateRhythm(0.016); // ~60fps fixed dt
}
drawCore();
drawGraph();
updateParticles();
// Draw Rhythm Overlay
if (currentState === STATE.PHASE6_GAME) {
drawRhythmGame();
}
requestAnimationFrame(loop);
}
function drawRhythmGame() {
const cx = width / 2;
const cy = height / 2;
// Draw Target Ring (CORE_RADIUS)
ctx.lineWidth = 4;
ctx.strokeStyle = '#ffffff';
ctx.globalAlpha = 0.5;
ctx.beginPath();
ctx.arc(cx, cy, CORE_RADIUS, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1.0;
// Draw Falling Circles
rhythmState.circles.forEach(c => {
ctx.lineWidth = 3;
ctx.strokeStyle = c.color;
ctx.beginPath();
ctx.arc(cx, cy, c.r, 0, Math.PI * 2);
ctx.stroke();
});
// Draw Energy Bar (Top Center)
const barW = 300;
const barH = 20;
const barX = cx - barW / 2;
const barY = 50;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(barX, barY, barW, barH);
ctx.strokeStyle = '#aaa';
ctx.strokeRect(barX, barY, barW, barH);
// Fill
const fillW = (rhythmState.energy / 100) * barW;
const fillCol = rhythmState.energy > 80 ? '#fbbf24' : '#22d3ee';
ctx.fillStyle = fillCol;
ctx.fillRect(barX, barY, fillW, barH);
// Text: Energy
ctx.fillStyle = '#fff';
ctx.font = '20px Orbitron';
ctx.textAlign = 'center';
ctx.fillText(`ENERGY: ${Math.floor(rhythmState.energy)}%`, cx, barY - 10);
// Text: Combo
if (rhythmState.combo > 1) {
ctx.fillStyle = '#f59e0b';
ctx.font = 'bold 40px Orbitron';
ctx.fillText(`${rhythmState.combo} COMBO`, cx, cy + 150);
}
// Floating Texts
drawFloatingTexts(cx, cy);
}
function drawFloatingTexts(cx, cy) {
for (let i = rhythmState.texts.length - 1; i >= 0; i--) {
const t = rhythmState.texts[i];
t.life -= 0.02;
t.y -= 1; // Rise up
if (t.life <= 0) {
rhythmState.texts.splice(i, 1);
continue;
}
ctx.save();
ctx.globalAlpha = t.life;
ctx.fillStyle = t.color;
ctx.font = 'bold 30px "Noto Sans TC"';
ctx.textAlign = 'center';
ctx.fillText(t.text, t.x, t.y);
ctx.restore();
}
}
function drawCore() {
// Move core to center in Phase 6+ (Rhythm Game & Summary)
let targetCx = width * 0.15;
if (currentState >= STATE.PHASE6_INTRO) {
targetCx = width / 2;
}
// Simple ease-in if we wanted, but direct assignment is safer for state changes
const cx = targetCx;
const cy = height * 0.5;
if (width < 768) ctx.globalAlpha = 0.2;
// Victory Effect: Big pulse if game won but not yet in summary
if (currentState >= STATE.PHASE6_GAME && rhythmState.energy >= 100) {
if (currentState === STATE.PHASE6_GAME) coreRotation += 0.2; // Spin faster only during game end
const pulse = 1 + Math.sin(Date.now() / 50) * 0.2; // Rapid pulse
ctx.shadowBlur = 50 * pulse;
ctx.shadowColor = '#fbbf24'; // Amber glow
} else {
coreRotation += 0.01;
ctx.shadowBlur = 0;
}
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(coreRotation);
ctx.strokeStyle = '#0891b2';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, 80, 0, Math.PI * 2);
ctx.stroke();
// Inner Hexagon
ctx.rotate(-coreRotation * 2);
ctx.fillStyle = rhythmState.energy >= 100 ? '#fbbf24' : '#22d3ee'; // Turn Gold on win
ctx.beginPath();
for (let i = 0; i < 6; i++) {
ctx.lineTo(50 * Math.cos(i * Math.PI / 3), 50 * Math.sin(i * Math.PI / 3));
}
ctx.fill();
ctx.restore();
ctx.globalAlpha = 1.0;
ctx.shadowBlur = 0; // Reset
}
function drawGraph() {
const padding = 60;
// Desktop: Graph takes up right 55% of screen. Mobile: Centered.
const isDesktop = width > 768;
const gw = isDesktop ? width * 0.53 : Math.min(600, width - 40);
const gh = isDesktop ? height * 0.6 : Math.min(400, height * 0.5);
// On Desktop, position graph on the right side (Start at 44% width)
// On Mobile, center it
const gx = isDesktop ? (width * 0.44) : (width - gw) / 2;
const gy = isDesktop ? height * 0.2 : height * 0.1;
// BG - Darker background for graph area to make it pop against the global image
ctx.fillStyle = 'rgba(5, 16, 21, 0.85)';
ctx.fillRect(gx, gy, gw, gh);
ctx.strokeStyle = '#475569';
ctx.strokeRect(gx, gy, gw, gh);
const ox = gx + padding;
const oy = gy + gh - padding;
const drawW = gw - padding * 2;
const drawH = gh - padding * 2;
const unitX = drawW / graphScale.xMax;
const unitY = drawH / graphScale.yMax;
ctx.beginPath();
ctx.strokeStyle = '#94a3b8';
ctx.lineWidth = 2;
ctx.moveTo(ox, oy); ctx.lineTo(ox + drawW, oy);
ctx.moveTo(ox, oy); ctx.lineTo(ox, oy - drawH);
ctx.stroke();
// Grid
ctx.textAlign = 'center';
ctx.font = 'bold 20px "Orbitron", sans-serif';
const jumpX = graphScale.xMax > 20 ? 10 : 1;
for (let i = 0; i <= graphScale.xMax; i += jumpX) {
const x = ox + i * unitX;
ctx.fillStyle = '#fbbf24'; // Amber X
ctx.fillText(i, x, oy + 20);
if (i > 0) {
ctx.beginPath(); ctx.moveTo(x, oy); ctx.lineTo(x, oy - drawH);
ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.stroke();
}
}
const jumpY = graphScale.yMax > 50 ? 20 : 2;
for (let i = 0; i <= graphScale.yMax; i += jumpY) {
const y = oy - i * unitY;
ctx.fillStyle = '#22d3ee'; // Cyan Y
ctx.fillText(i, ox - 20, y + 4);
if (i > 0) {
ctx.beginPath(); ctx.moveTo(ox, y); ctx.lineTo(ox + drawW, y);
ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.stroke();
}
}
ctx.fillStyle = '#fbbf24'; ctx.font = 'bold 16px sans-serif';
ctx.fillText("晶石數 (x)", ox + drawW / 2, oy + 40);
ctx.save();
ctx.translate(ox - 45, oy - drawH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = '#22d3ee';
ctx.fillText("電力 (y)", 0, 0);
ctx.restore();
if (currentState >= STATE.PHASE2_PREDICTION_INTRO && currentState < STATE.PHASE4_INTRO) {
ctx.beginPath();
ctx.strokeStyle = '#22d3ee';
ctx.lineWidth = 2;
const endX = graphScale.xMax;
const endY = 2 * endX + 3;
ctx.moveTo(ox, oy - 3 * unitY);
ctx.lineTo(ox + endX * unitX, oy - endY * unitY);
ctx.stroke();
}
// Phase 4: Draw line y = 3x + 5 (After finding a)
if (currentState >= STATE.PHASE4_VERIFY) {
ctx.beginPath();
ctx.strokeStyle = '#fbbf24'; // Amber Line for new formula
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]); // Dashed line to show it's a prediction/new model
const endX = graphScale.xMax;
const endY = 3 * endX + 5;
ctx.moveTo(ox, oy - 5 * unitY); // Start at (0, 5)
ctx.lineTo(ox + endX * unitX, oy - endY * unitY);
ctx.stroke();
ctx.setLineDash([]); // Reset dash
}
collectedPoints.forEach(p => {
const px = ox + p.x * unitX;
const py = oy - p.y * unitY;
ctx.beginPath();
ctx.arc(px, py, 6, 0, Math.PI * 2);
ctx.fillStyle = '#fbbf24';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.stroke();
// Blinking Effect (Requested)
if (p.blink) {
const alpha = 0.5 + 0.5 * Math.sin(Date.now() / 150);
ctx.beginPath();
ctx.arc(px, py, 12 + 4 * Math.sin(Date.now() / 150), 0, Math.PI * 2);
ctx.strokeStyle = `rgba(34, 211, 238, ${alpha})`; // Cyan pulsing ring
ctx.lineWidth = 3;
ctx.stroke();
}
// Color coded coordinates: (x, y)
ctx.font = 'bold 24px Orbitron'; // Point Numbers Increased
const textY = py - 24; // Position higher up for larger font
// Measure text to center it
const strX = `${p.x}`;
const strY = `${p.y}`;
const w1 = ctx.measureText("(").width;
const wX = ctx.measureText(strX).width;
const wComma = ctx.measureText(", ").width;
const wY = ctx.measureText(strY).width;
const w2 = ctx.measureText(")").width;
const totalW = w1 + wX + wComma + wY + w2;
let cursorX = px - totalW / 2;
ctx.fillStyle = '#cbd5e1'; ctx.fillText("(", cursorX + w1 / 2, textY); cursorX += w1;
ctx.fillStyle = '#fbbf24'; ctx.fillText(strX, cursorX + wX / 2, textY); cursorX += wX; // Amber X
ctx.fillStyle = '#cbd5e1'; ctx.fillText(", ", cursorX + wComma / 2, textY); cursorX += wComma;
ctx.fillStyle = '#22d3ee'; ctx.fillText(strY, cursorX + wY / 2, textY); cursorX += wY; // Cyan Y
ctx.fillStyle = '#cbd5e1'; ctx.fillText(")", cursorX + w2 / 2, textY);
});
// Draw Guide Arrows if active (Coordinate Animation)
if (guideArrowState.active && (currentState === STATE.PHASE2_COORDINATE || currentState === STATE.PHASE2_Q)) {
drawGuideArrows();
}
}
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.02;
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
ctx.fill();
if (p.life <= 0) particles.splice(i, 1);
}
ctx.globalAlpha = 1.0;
}
</script>
<!-- Credits -->
<div class="fixed bottom-4 right-4 text-right text-slate-500 text-xs z-50 pointer-events-auto select-none">
<div class="mb-1">遊戲設計:新竹縣精華國中藍星宇</div>
<div>FB教育社群:<a href="https://www.facebook.com/groups/1554372228718393" target="_blank"
class="hover:text-cyan-400 transition-colors">萬物皆數</a></div>
</div>
<script>
// --- Main Game Loop (Overridden/Appended) ---
let lastTime = Date.now();
function loop() {
const now = Date.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
ctx.clearRect(0, 0, width, height);
if (currentState === STATE.PHASE6_GAME) {
// Rhythm Game Mode
updateRhythm(dt);
drawRhythmBackground();
drawRhythmGame();
drawCore();
} else if (currentState >= STATE.PHASE6_INTRO) {
// Intro & Summary Mode (Use Rhythm BG)
drawRhythmBackground();
drawCore();
} else {
// Standard Graph Mode
drawGraph();
drawCore();
}
updateParticles();
requestAnimationFrame(loop);
}
</script>
</body>
</html>