Spaces:
Running
Running
Upload 2 files
Browse files- function.html +89 -12
- sequence.html +100 -9
function.html
CHANGED
|
@@ -208,6 +208,18 @@
|
|
| 208 |
<!-- Background Canvas -->
|
| 209 |
<canvas id="gameCanvas"></canvas>
|
| 210 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
<!-- Main UI Container -->
|
| 212 |
<div id="ui-container" class="fixed inset-0 z-10 pointer-events-none">
|
| 213 |
<!--
|
|
@@ -299,14 +311,74 @@
|
|
| 299 |
targetInput: null,
|
| 300 |
init: function () {
|
| 301 |
this.element = document.getElementById('virtual-keypad');
|
|
|
|
|
|
|
|
|
|
| 302 |
// Auto-close when clicking outside
|
| 303 |
document.addEventListener('click', (e) => {
|
| 304 |
if (this.element.classList.contains('active') &&
|
| 305 |
!this.element.contains(e.target) &&
|
| 306 |
-
e.target !== this.targetInput
|
|
|
|
| 307 |
this.close();
|
| 308 |
}
|
| 309 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
},
|
| 311 |
open: function (inputElement) {
|
| 312 |
this.targetInput = inputElement;
|
|
@@ -680,6 +752,7 @@
|
|
| 680 |
|
| 681 |
// --- State Management ---
|
| 682 |
function enterPhase(phase) {
|
|
|
|
| 683 |
currentState = phase;
|
| 684 |
updateUI();
|
| 685 |
}
|
|
@@ -747,8 +820,8 @@
|
|
| 747 |
<div class="font-tech text-3xl md:text-6xl mb-6">
|
| 748 |
<span class="text-cyan-400">y</span> = 2<span class="text-amber-400">x</span> + 3
|
| 749 |
</div>
|
| 750 |
-
<p class="text-slate-300 text-
|
| 751 |
-
每投入 <span class="text-amber-400 font-bold text-
|
| 752 |
</p>
|
| 753 |
</div>
|
| 754 |
<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">
|
|
@@ -787,7 +860,7 @@
|
|
| 787 |
<div class="text-slate-400 font-bold text-xl border-b border-slate-500/30 pb-2">2. 坐標描點</div>
|
| 788 |
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/30 flex flex-col items-center justify-center flex-grow">
|
| 789 |
<div class="text-sm text-slate-500 mb-1">對應座標</div>
|
| 790 |
-
<div class="font-tech text-
|
| 791 |
(<span class="text-amber-400">${xVal}</span>, <span class="text-cyan-400">y</span>)
|
| 792 |
</div>
|
| 793 |
</div>
|
|
@@ -922,16 +995,16 @@
|
|
| 922 |
case STATE.SUCCESS:
|
| 923 |
html = `
|
| 924 |
<div class="text-center">
|
| 925 |
-
<h2 class="text-
|
| 926 |
<p class="text-slate-300 mb-6 text-xl">預測成功!核心運作恢復正常。</p>
|
| 927 |
|
| 928 |
<!-- Teaching Content -->
|
| 929 |
<div class="bg-slate-800/80 p-4 rounded-xl border border-cyan-500/30 mb-6 text-left">
|
| 930 |
-
<h3 class="text-lg md:text-
|
| 931 |
-
<p class="text-slate-200 text-base md:text-
|
| 932 |
-
剛剛我們使用的「能量轉換法則」,在數學上就叫做<span class="text-amber-400 font-bold text-xl md:text-
|
| 933 |
</p>
|
| 934 |
-
<p class="text-slate-300 text-base md:text-
|
| 935 |
函數就像一座<span class="text-cyan-400 font-bold">工廠</span>:<br>
|
| 936 |
它會把原料 <span class="font-bold text-amber-400">x (晶石)</span>,藉由固定的規則,<br>轉換成產品 <span class="font-bold text-cyan-400">y (電力)</span>。
|
| 937 |
</p>
|
|
@@ -1142,10 +1215,10 @@
|
|
| 1142 |
|
| 1143 |
html = `
|
| 1144 |
<div class="text-center w-full h-full flex flex-col items-center">
|
| 1145 |
-
<h2 class="text-
|
| 1146 |
任務完成 SYSTEM RESTORED
|
| 1147 |
</h2>
|
| 1148 |
-
<div class="text-amber-400 font-tech text-
|
| 1149 |
|
| 1150 |
<!-- Score Board -->
|
| 1151 |
<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">
|
|
@@ -1232,6 +1305,7 @@
|
|
| 1232 |
|
| 1233 |
// --- Logic Operations ---
|
| 1234 |
function startQPhase() {
|
|
|
|
| 1235 |
qStep = 0;
|
| 1236 |
enterPhase(STATE.PHASE2_Q);
|
| 1237 |
}
|
|
@@ -1436,6 +1510,7 @@
|
|
| 1436 |
if (val === 35) {
|
| 1437 |
playSound('success');
|
| 1438 |
// Auto verification visual
|
|
|
|
| 1439 |
addPoint(10, 35);
|
| 1440 |
updateUI("預測正確!投入 10 顆晶石,電力確實為 35!<br><span class='text-amber-400 text-base'>(確認點連成一線...)</span>");
|
| 1441 |
|
|
@@ -1471,6 +1546,7 @@
|
|
| 1471 |
const b = parseInt(document.getElementById('final-b').value);
|
| 1472 |
|
| 1473 |
if (a === 3 && b === 5) {
|
|
|
|
| 1474 |
playSound('success');
|
| 1475 |
// Instead of alert, go to Phase 6
|
| 1476 |
enterPhase(STATE.PHASE6_INTRO);
|
|
@@ -1650,9 +1726,10 @@
|
|
| 1650 |
rhythmState.combo++;
|
| 1651 |
spawnFloatingText("PERFECT! +10%", '#fbbf24'); // Gold
|
| 1652 |
playSound('hit_perfect');
|
|
|
|
| 1653 |
} else {
|
| 1654 |
rhythmState.energy = Math.min(100, rhythmState.energy + 5);
|
| 1655 |
-
rhythmState.score +=
|
| 1656 |
rhythmState.combo++;
|
| 1657 |
spawnFloatingText("GOOD +5%", '#22d3ee'); // Blue
|
| 1658 |
playSound('hit_good');
|
|
|
|
| 208 |
<!-- Background Canvas -->
|
| 209 |
<canvas id="gameCanvas"></canvas>
|
| 210 |
|
| 211 |
+
<!-- UI HUD -->
|
| 212 |
+
<div class="fixed top-4 right-4 z-50">
|
| 213 |
+
<a href="index.html"
|
| 214 |
+
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">
|
| 215 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
| 216 |
+
stroke="currentColor">
|
| 217 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 218 |
+
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" />
|
| 219 |
+
</svg>
|
| 220 |
+
</a>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
<!-- Main UI Container -->
|
| 224 |
<div id="ui-container" class="fixed inset-0 z-10 pointer-events-none">
|
| 225 |
<!--
|
|
|
|
| 311 |
targetInput: null,
|
| 312 |
init: function () {
|
| 313 |
this.element = document.getElementById('virtual-keypad');
|
| 314 |
+
// Prevent click bubbling from keypad to document (stops accidental closes if logic fails)
|
| 315 |
+
this.element.addEventListener('click', (e) => e.stopPropagation());
|
| 316 |
+
|
| 317 |
// Auto-close when clicking outside
|
| 318 |
document.addEventListener('click', (e) => {
|
| 319 |
if (this.element.classList.contains('active') &&
|
| 320 |
!this.element.contains(e.target) &&
|
| 321 |
+
e.target !== this.targetInput &&
|
| 322 |
+
!e.target.classList.contains('keypad-btn')) {
|
| 323 |
this.close();
|
| 324 |
}
|
| 325 |
});
|
| 326 |
+
|
| 327 |
+
// --- Drag Logic ---
|
| 328 |
+
const handle = this.element.querySelector('.keypad-handle');
|
| 329 |
+
this.isDragging = false;
|
| 330 |
+
this.startX = 0; this.startY = 0;
|
| 331 |
+
this.offsetX = 0; this.offsetY = 0;
|
| 332 |
+
|
| 333 |
+
const startDrag = (e) => {
|
| 334 |
+
// Prevent text selection/scrolling on touch
|
| 335 |
+
if (e.type === 'touchstart' && e.cancelable) e.preventDefault();
|
| 336 |
+
|
| 337 |
+
this.isDragging = true;
|
| 338 |
+
this.element.classList.add('dragging');
|
| 339 |
+
|
| 340 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 341 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 342 |
+
|
| 343 |
+
const rect = this.element.getBoundingClientRect();
|
| 344 |
+
this.offsetX = clientX - rect.left;
|
| 345 |
+
this.offsetY = clientY - rect.top;
|
| 346 |
+
|
| 347 |
+
// Add listeners to document for smooth dragging outside element
|
| 348 |
+
document.addEventListener(e.type.includes('mouse') ? 'mousemove' : 'touchmove', doDrag, { passive: false });
|
| 349 |
+
document.addEventListener(e.type.includes('mouse') ? 'mouseup' : 'touchend', endDrag);
|
| 350 |
+
};
|
| 351 |
+
|
| 352 |
+
const doDrag = (e) => {
|
| 353 |
+
if (!this.isDragging) return;
|
| 354 |
+
e.preventDefault(); // Critical for stopping scroll on mobile
|
| 355 |
+
|
| 356 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 357 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 358 |
+
|
| 359 |
+
// Absolute positioning
|
| 360 |
+
this.element.style.transform = 'none';
|
| 361 |
+
this.element.style.bottom = 'auto'; // Clear bottom
|
| 362 |
+
this.element.style.right = 'auto'; // Clear right
|
| 363 |
+
this.element.style.left = (clientX - this.offsetX) + 'px';
|
| 364 |
+
this.element.style.top = (clientY - this.offsetY) + 'px';
|
| 365 |
+
};
|
| 366 |
+
|
| 367 |
+
const endDrag = (e) => {
|
| 368 |
+
this.isDragging = false;
|
| 369 |
+
this.element.classList.remove('dragging');
|
| 370 |
+
document.removeEventListener('mousemove', doDrag);
|
| 371 |
+
document.removeEventListener('touchmove', doDrag);
|
| 372 |
+
document.removeEventListener('mouseup', endDrag);
|
| 373 |
+
document.removeEventListener('touchend', endDrag);
|
| 374 |
+
};
|
| 375 |
+
|
| 376 |
+
// Attach start listeners
|
| 377 |
+
if (handle) {
|
| 378 |
+
handle.addEventListener('mousedown', startDrag);
|
| 379 |
+
handle.addEventListener('touchstart', startDrag, { passive: false });
|
| 380 |
+
handle.style.cursor = 'grab';
|
| 381 |
+
}
|
| 382 |
},
|
| 383 |
open: function (inputElement) {
|
| 384 |
this.targetInput = inputElement;
|
|
|
|
| 752 |
|
| 753 |
// --- State Management ---
|
| 754 |
function enterPhase(phase) {
|
| 755 |
+
if (window.keypad) keypad.close(); // Auto-close keypad on any phase change
|
| 756 |
currentState = phase;
|
| 757 |
updateUI();
|
| 758 |
}
|
|
|
|
| 820 |
<div class="font-tech text-3xl md:text-6xl mb-6">
|
| 821 |
<span class="text-cyan-400">y</span> = 2<span class="text-amber-400">x</span> + 3
|
| 822 |
</div>
|
| 823 |
+
<p class="text-slate-300 text-lg md:text-xl leading-relaxed font-bold">
|
| 824 |
+
每投入 <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> 點電力。
|
| 825 |
</p>
|
| 826 |
</div>
|
| 827 |
<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">
|
|
|
|
| 860 |
<div class="text-slate-400 font-bold text-xl border-b border-slate-500/30 pb-2">2. 坐標描點</div>
|
| 861 |
<div class="bg-slate-800/30 p-4 rounded-xl border border-slate-700/30 flex flex-col items-center justify-center flex-grow">
|
| 862 |
<div class="text-sm text-slate-500 mb-1">對應座標</div>
|
| 863 |
+
<div class="font-tech text-lg md:text-xl font-bold text-slate-500">
|
| 864 |
(<span class="text-amber-400">${xVal}</span>, <span class="text-cyan-400">y</span>)
|
| 865 |
</div>
|
| 866 |
</div>
|
|
|
|
| 995 |
case STATE.SUCCESS:
|
| 996 |
html = `
|
| 997 |
<div class="text-center">
|
| 998 |
+
<h2 class="text-2xl md:text-2xl font-bold text-green-400 mb-2 font-tech">SYSTEM STABILIZED</h2>
|
| 999 |
<p class="text-slate-300 mb-6 text-xl">預測成功!核心運作恢復正常。</p>
|
| 1000 |
|
| 1001 |
<!-- Teaching Content -->
|
| 1002 |
<div class="bg-slate-800/80 p-4 rounded-xl border border-cyan-500/30 mb-6 text-left">
|
| 1003 |
+
<h3 class="text-lg md:text-2xl font-bold text-white mb-2 text-center">任務小結:什麼是「函數」?</h3>
|
| 1004 |
+
<p class="text-slate-200 text-base md:text-xl leading-relaxed mb-3">
|
| 1005 |
+
剛剛我們使用的「能量轉換法則」,在數學上就叫做<span class="text-amber-400 font-bold text-xl md:text-2xl mx-1">「函數」(Function)</span>。
|
| 1006 |
</p>
|
| 1007 |
+
<p class="text-slate-300 text-base md:text-xl leading-relaxed">
|
| 1008 |
函數就像一座<span class="text-cyan-400 font-bold">工廠</span>:<br>
|
| 1009 |
它會把原料 <span class="font-bold text-amber-400">x (晶石)</span>,藉由固定的規則,<br>轉換成產品 <span class="font-bold text-cyan-400">y (電力)</span>。
|
| 1010 |
</p>
|
|
|
|
| 1215 |
|
| 1216 |
html = `
|
| 1217 |
<div class="text-center w-full h-full flex flex-col items-center">
|
| 1218 |
+
<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">
|
| 1219 |
任務完成 SYSTEM RESTORED
|
| 1220 |
</h2>
|
| 1221 |
+
<div class="text-amber-400 font-tech text-base md:text-2xl mb-10 tracking-[0.5em]">MISSION ACCOMPLISHED</div>
|
| 1222 |
|
| 1223 |
<!-- Score Board -->
|
| 1224 |
<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">
|
|
|
|
| 1305 |
|
| 1306 |
// --- Logic Operations ---
|
| 1307 |
function startQPhase() {
|
| 1308 |
+
if (window.keypad) keypad.close();
|
| 1309 |
qStep = 0;
|
| 1310 |
enterPhase(STATE.PHASE2_Q);
|
| 1311 |
}
|
|
|
|
| 1510 |
if (val === 35) {
|
| 1511 |
playSound('success');
|
| 1512 |
// Auto verification visual
|
| 1513 |
+
if (window.keypad) keypad.close();
|
| 1514 |
addPoint(10, 35);
|
| 1515 |
updateUI("預測正確!投入 10 顆晶石,電力確實為 35!<br><span class='text-amber-400 text-base'>(確認點連成一線...)</span>");
|
| 1516 |
|
|
|
|
| 1546 |
const b = parseInt(document.getElementById('final-b').value);
|
| 1547 |
|
| 1548 |
if (a === 3 && b === 5) {
|
| 1549 |
+
if (window.keypad) keypad.close();
|
| 1550 |
playSound('success');
|
| 1551 |
// Instead of alert, go to Phase 6
|
| 1552 |
enterPhase(STATE.PHASE6_INTRO);
|
|
|
|
| 1726 |
rhythmState.combo++;
|
| 1727 |
spawnFloatingText("PERFECT! +10%", '#fbbf24'); // Gold
|
| 1728 |
playSound('hit_perfect');
|
| 1729 |
+
playSound('hit_perfect');
|
| 1730 |
} else {
|
| 1731 |
rhythmState.energy = Math.min(100, rhythmState.energy + 5);
|
| 1732 |
+
rhythmState.score += 50; // User Request: 50 points for GOOD
|
| 1733 |
rhythmState.combo++;
|
| 1734 |
spawnFloatingText("GOOD +5%", '#22d3ee'); // Blue
|
| 1735 |
playSound('hit_good');
|
sequence.html
CHANGED
|
@@ -921,6 +921,9 @@
|
|
| 921 |
targetInput: null,
|
| 922 |
init: function () {
|
| 923 |
this.element = document.getElementById('virtual-keypad');
|
|
|
|
|
|
|
|
|
|
| 924 |
// Auto-close when clicking outside
|
| 925 |
document.addEventListener('click', (e) => {
|
| 926 |
if (this.element.classList.contains('active') &&
|
|
@@ -930,6 +933,60 @@
|
|
| 930 |
this.close();
|
| 931 |
}
|
| 932 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
},
|
| 934 |
open: function (inputElement) {
|
| 935 |
this.targetInput = inputElement;
|
|
@@ -999,24 +1056,49 @@
|
|
| 999 |
|
| 1000 |
// --- Tutorial ---
|
| 1001 |
function startTutorial() {
|
|
|
|
| 1002 |
gameState = STATE.TUTORIAL;
|
| 1003 |
document.getElementById('ui-hud').classList.remove('hidden');
|
| 1004 |
document.getElementById('ui-tutorial').classList.remove('hidden');
|
| 1005 |
resetWorld();
|
| 1006 |
const groundY = height * 0.7;
|
|
|
|
|
|
|
| 1007 |
platforms.push({ x: 0, y: groundY, w: 1000, h: 40, type: 'safe', visited: true });
|
| 1008 |
-
|
| 1009 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
tutorial.step = 0;
|
| 1011 |
updateTutorialUI("基礎移動", "請往右移動,觸碰黃色光柱");
|
| 1012 |
if (!isLooping) { lastTime = performance.now(); loop(lastTime); }
|
| 1013 |
}
|
| 1014 |
|
| 1015 |
function advanceTutorialToLeft() {
|
|
|
|
| 1016 |
playSound('collect');
|
| 1017 |
tutorial.step = 1; checkpoints = [];
|
| 1018 |
-
|
| 1019 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1020 |
updateTutorialUI("折返跑", "做得好!現在請折返往左移動");
|
| 1021 |
}
|
| 1022 |
|
|
@@ -1030,14 +1112,23 @@
|
|
| 1030 |
const startX = Math.max(player.x + 400, 600);
|
| 1031 |
const groundY = height * 0.7;
|
| 1032 |
|
| 1033 |
-
// Create a
|
| 1034 |
-
platforms.push({ x:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
|
| 1036 |
// Adjusted Heights: Restored to enforce Double/Triple jumps
|
| 1037 |
// 140 (Single), 290 (Double Only), 430 (Triple Only)
|
| 1038 |
-
tutorialObjects.push({ x:
|
| 1039 |
-
tutorialObjects.push({ x:
|
| 1040 |
-
tutorialObjects.push({ x:
|
| 1041 |
}
|
| 1042 |
|
| 1043 |
// --- Quiz ---
|
|
|
|
| 921 |
targetInput: null,
|
| 922 |
init: function () {
|
| 923 |
this.element = document.getElementById('virtual-keypad');
|
| 924 |
+
// Prevent click bubbling from keypad to document (stops accidental closes if logic fails)
|
| 925 |
+
this.element.addEventListener('click', (e) => e.stopPropagation());
|
| 926 |
+
|
| 927 |
// Auto-close when clicking outside
|
| 928 |
document.addEventListener('click', (e) => {
|
| 929 |
if (this.element.classList.contains('active') &&
|
|
|
|
| 933 |
this.close();
|
| 934 |
}
|
| 935 |
});
|
| 936 |
+
|
| 937 |
+
// --- Drag Logic ---
|
| 938 |
+
const handle = this.element.querySelector('.keypad-handle');
|
| 939 |
+
this.isDragging = false;
|
| 940 |
+
this.startX = 0; this.startY = 0;
|
| 941 |
+
this.offsetX = 0; this.offsetY = 0;
|
| 942 |
+
|
| 943 |
+
const startDrag = (e) => {
|
| 944 |
+
// Prevent text selection/scrolling on touch
|
| 945 |
+
if (e.type === 'touchstart' && e.cancelable) e.preventDefault();
|
| 946 |
+
|
| 947 |
+
this.isDragging = true;
|
| 948 |
+
this.element.classList.add('dragging');
|
| 949 |
+
|
| 950 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 951 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 952 |
+
|
| 953 |
+
const rect = this.element.getBoundingClientRect();
|
| 954 |
+
this.offsetX = clientX - rect.left;
|
| 955 |
+
this.offsetY = clientY - rect.top;
|
| 956 |
+
|
| 957 |
+
// Add listeners to document
|
| 958 |
+
document.addEventListener(e.type.includes('mouse') ? 'mousemove' : 'touchmove', doDrag, { passive: false });
|
| 959 |
+
document.addEventListener(e.type.includes('mouse') ? 'mouseup' : 'touchend', endDrag);
|
| 960 |
+
};
|
| 961 |
+
|
| 962 |
+
const doDrag = (e) => {
|
| 963 |
+
if (!this.isDragging) return;
|
| 964 |
+
e.preventDefault();
|
| 965 |
+
|
| 966 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 967 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 968 |
+
|
| 969 |
+
this.element.style.transform = 'none';
|
| 970 |
+
this.element.style.bottom = 'auto';
|
| 971 |
+
this.element.style.right = 'auto';
|
| 972 |
+
this.element.style.left = (clientX - this.offsetX) + 'px';
|
| 973 |
+
this.element.style.top = (clientY - this.offsetY) + 'px';
|
| 974 |
+
};
|
| 975 |
+
|
| 976 |
+
const endDrag = (e) => {
|
| 977 |
+
this.isDragging = false;
|
| 978 |
+
this.element.classList.remove('dragging');
|
| 979 |
+
document.removeEventListener('mousemove', doDrag);
|
| 980 |
+
document.removeEventListener('touchmove', doDrag);
|
| 981 |
+
document.removeEventListener('mouseup', endDrag);
|
| 982 |
+
document.removeEventListener('touchend', endDrag);
|
| 983 |
+
};
|
| 984 |
+
|
| 985 |
+
if (handle) {
|
| 986 |
+
handle.addEventListener('mousedown', startDrag);
|
| 987 |
+
handle.addEventListener('touchstart', startDrag, { passive: false });
|
| 988 |
+
handle.style.cursor = 'grab';
|
| 989 |
+
}
|
| 990 |
},
|
| 991 |
open: function (inputElement) {
|
| 992 |
this.targetInput = inputElement;
|
|
|
|
| 1056 |
|
| 1057 |
// --- Tutorial ---
|
| 1058 |
function startTutorial() {
|
| 1059 |
+
if (window.keypad) keypad.close(); // Auto-close
|
| 1060 |
gameState = STATE.TUTORIAL;
|
| 1061 |
document.getElementById('ui-hud').classList.remove('hidden');
|
| 1062 |
document.getElementById('ui-tutorial').classList.remove('hidden');
|
| 1063 |
resetWorld();
|
| 1064 |
const groundY = height * 0.7;
|
| 1065 |
+
|
| 1066 |
+
// Standard Platform: 0 to 1000
|
| 1067 |
platforms.push({ x: 0, y: groundY, w: 1000, h: 40, type: 'safe', visited: true });
|
| 1068 |
+
|
| 1069 |
+
// Player Start (Fixed 200)
|
| 1070 |
+
player.x = 200;
|
| 1071 |
+
player.y = groundY - 54;
|
| 1072 |
+
player.isGrounded = true; player.prevY = player.y; cameraX = 0; cameraY = 0;
|
| 1073 |
+
|
| 1074 |
+
// Checkpoint Right (Fixed 800) - Guaranteed on platform
|
| 1075 |
+
checkpoints = [{ x: 800, y: groundY - 40, w: 40, h: 40, active: true, type: 'right' }];
|
| 1076 |
+
|
| 1077 |
tutorial.step = 0;
|
| 1078 |
updateTutorialUI("基礎移動", "請往右移動,觸碰黃色光柱");
|
| 1079 |
if (!isLooping) { lastTime = performance.now(); loop(lastTime); }
|
| 1080 |
}
|
| 1081 |
|
| 1082 |
function advanceTutorialToLeft() {
|
| 1083 |
+
if (window.keypad) keypad.close(); // Auto-close
|
| 1084 |
playSound('collect');
|
| 1085 |
tutorial.step = 1; checkpoints = [];
|
| 1086 |
+
|
| 1087 |
+
// Fix Floating Issue: Find the actual existing safe platform
|
| 1088 |
+
// Do NOT recalculate from height * 0.7 which might change on resize
|
| 1089 |
+
const platform = platforms.find(p => p.type === 'safe' && p.x === 0);
|
| 1090 |
+
const groundY = platform ? platform.y : height * 0.7;
|
| 1091 |
+
|
| 1092 |
+
// Checkpoint Left (Fixed 200) - Symmetric to Start (800)
|
| 1093 |
+
checkpoints.push({
|
| 1094 |
+
x: 200,
|
| 1095 |
+
y: groundY - 40,
|
| 1096 |
+
w: 40,
|
| 1097 |
+
h: 40,
|
| 1098 |
+
active: true,
|
| 1099 |
+
type: 'left'
|
| 1100 |
+
});
|
| 1101 |
+
|
| 1102 |
updateTutorialUI("折返跑", "做得好!現在請折返往左移動");
|
| 1103 |
}
|
| 1104 |
|
|
|
|
| 1112 |
const startX = Math.max(player.x + 400, 600);
|
| 1113 |
const groundY = height * 0.7;
|
| 1114 |
|
| 1115 |
+
// Create a MASSIVE platform to prevent falling anywhere
|
| 1116 |
+
platforms.push({ x: -2000, y: groundY, w: 6000, h: 40, type: 'safe' });
|
| 1117 |
+
|
| 1118 |
+
// FORCE Player Stats to prevent falling (Feedback #2)
|
| 1119 |
+
player.y = groundY - player.h - 1;
|
| 1120 |
+
player.vy = 0;
|
| 1121 |
+
player.isGrounded = true;
|
| 1122 |
+
player.isJumping = false;
|
| 1123 |
+
|
| 1124 |
+
// Recalculate startX based on new platform start
|
| 1125 |
+
const safeStartX = player.x + 300;
|
| 1126 |
|
| 1127 |
// Adjusted Heights: Restored to enforce Double/Triple jumps
|
| 1128 |
// 140 (Single), 290 (Double Only), 430 (Triple Only)
|
| 1129 |
+
tutorialObjects.push({ x: safeStartX, y: groundY - 140, w: 50, h: 50, type: 'target_jump', id: 1, hit: false, color: '#facc15', label: '1' }); // Yellow-400
|
| 1130 |
+
tutorialObjects.push({ x: safeStartX + 300, y: groundY - 290, w: 50, h: 50, type: 'target_jump', id: 2, hit: false, color: '#fbbf24', label: '2' }); // Amber-400
|
| 1131 |
+
tutorialObjects.push({ x: safeStartX + 600, y: groundY - 430, w: 50, h: 50, type: 'target_jump', id: 3, hit: false, color: '#f59e0b', label: '3' }); // Amber-500
|
| 1132 |
}
|
| 1133 |
|
| 1134 |
// --- Quiz ---
|