Lashtw commited on
Commit
c7aa405
·
verified ·
1 Parent(s): c33a50c

Upload 2 files

Browse files
Files changed (2) hide show
  1. function.html +89 -12
  2. 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-xl md:text-4xl leading-relaxed font-bold">
751
- 每投入 <span class="text-amber-400 font-bold text-4xl md:text-7xl mx-2 border-b-2 border-amber-400/50">x</span> 顆晶石,<br>就會產生 <span class="text-cyan-400 font-bold text-4xl md:text-7xl mx-2 border-b-2 border-cyan-400/50">y</span> 點電力。
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-xl md:text-3xl font-bold text-slate-500">
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-4xl font-bold text-green-400 mb-2 font-tech">SYSTEM STABILIZED</h2>
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-3xl font-bold text-white mb-2 text-center">任務小結:什麼是「函數」?</h3>
931
- <p class="text-slate-200 text-base md:text-2xl leading-relaxed mb-3">
932
- 剛剛我們使用的「能量轉換法則」,在數學上就叫做<span class="text-amber-400 font-bold text-xl md:text-3xl mx-1">「函數」(Function)</span>。
933
  </p>
934
- <p class="text-slate-300 text-base md:text-2xl leading-relaxed">
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-2xl md:text-6xl 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">
1146
  任務完成 SYSTEM RESTORED
1147
  </h2>
1148
- <div class="text-amber-400 font-tech text-lg md:text-4xl mb-10 tracking-[0.5em]">MISSION ACCOMPLISHED</div>
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 += 100;
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
- player.x = 200; player.y = groundY - 54; player.isGrounded = true; player.prevY = player.y; cameraX = 0; cameraY = 0;
1009
- checkpoints = [{ x: 600, y: groundY - 60, w: 40, h: 60, active: true, type: 'right' }];
 
 
 
 
 
 
 
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
- const groundY = height * 0.7;
1019
- checkpoints.push({ x: 200, y: groundY - 60, w: 40, h: 60, active: true, type: 'left' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 long platform for the training
1034
- platforms.push({ x: startX - 200, y: groundY, w: 1500, h: 40, type: 'safe' });
 
 
 
 
 
 
 
 
 
1035
 
1036
  // Adjusted Heights: Restored to enforce Double/Triple jumps
1037
  // 140 (Single), 290 (Double Only), 430 (Triple Only)
1038
- tutorialObjects.push({ x: startX, y: groundY - 140, w: 50, h: 50, type: 'target_jump', id: 1, hit: false, color: '#facc15', label: '1' }); // Yellow-400
1039
- tutorialObjects.push({ x: startX + 300, y: groundY - 290, w: 50, h: 50, type: 'target_jump', id: 2, hit: false, color: '#fbbf24', label: '2' }); // Amber-400
1040
- tutorialObjects.push({ x: startX + 600, y: groundY - 430, w: 50, h: 50, type: 'target_jump', id: 3, hit: false, color: '#f59e0b', label: '3' }); // Amber-500
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 ---