Spaces:
Running
Running
Upload 2 files
Browse files- congruence_detective.html +280 -7
- skyscraper.html +133 -24
congruence_detective.html
CHANGED
|
@@ -195,6 +195,29 @@
|
|
| 195 |
pointer-events: none;
|
| 196 |
}
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
/* Scanlines */
|
| 199 |
.scanlines {
|
| 200 |
background: linear-gradient(to bottom,
|
|
@@ -247,6 +270,109 @@
|
|
| 247 |
flex-direction: column;
|
| 248 |
justify-content: flex-start;
|
| 249 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
</style>
|
| 251 |
</head>
|
| 252 |
|
|
@@ -314,6 +440,18 @@
|
|
| 314 |
</div>
|
| 315 |
</div>
|
| 316 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
<script>
|
| 318 |
// Game Script
|
| 319 |
let playerName = localStorage.getItem('player_nickname') || '��鳥探員';
|
|
@@ -321,6 +459,124 @@
|
|
| 321 |
// Initial Script Placeholder - populated dynamically
|
| 322 |
let SCRIPT = [];
|
| 323 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
class GameEngine {
|
| 325 |
constructor() {
|
| 326 |
this.step = 0;
|
|
@@ -1320,16 +1576,14 @@
|
|
| 1320 |
|
| 1321 |
quizLayer.classList.remove('hidden');
|
| 1322 |
|
| 1323 |
-
// Integrated layout matching dialogue style exactly
|
| 1324 |
-
|
| 1325 |
// Integrated layout matching dialogue style exactly
|
| 1326 |
quizContent.innerHTML = `
|
| 1327 |
<div class="dialogue-name">資深警探 <span class="text-xs text-slate-500 ml-2 tracking-normal">// AUTHENTICATION REQUIRED</span></div>
|
| 1328 |
<div class="dialogue-text mb-4">${data.question}</div>
|
| 1329 |
|
| 1330 |
<div class="flex gap-4 w-full mt-auto items-end">
|
| 1331 |
-
<div class="text-fuchsia-500 font-tech text-xl animate-pulse">
|
| 1332 |
-
<input type="text" id="quiz-input" class="flex-1 bg-transparent border-b-2 border-fuchsia-500/50 text-white text-xl p-2 focus:outline-none focus:border-fuchsia-400 font-mono tracking-wider placeholder-slate-600" placeholder="輸入答案..." autocomplete="off">
|
| 1333 |
<button id="quiz-submit" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold px-8 py-2 rounded clip-path-slant text-lg font-tech tracking-widest border border-fuchsia-400/50 transition-all shadow-lg shadow-fuchsia-500/20">CONFIRM</button>
|
| 1334 |
</div>
|
| 1335 |
<div id="quiz-feedback" class="absolute top-6 right-8 font-tech text-xl font-bold"></div>
|
|
@@ -1339,8 +1593,6 @@
|
|
| 1339 |
const btn = document.getElementById('quiz-submit');
|
| 1340 |
const feedback = document.getElementById('quiz-feedback');
|
| 1341 |
|
| 1342 |
-
input.focus();
|
| 1343 |
-
|
| 1344 |
const checkAnswer = () => {
|
| 1345 |
const val = input.value.trim();
|
| 1346 |
if (data.answer.includes(val)) {
|
|
@@ -1349,6 +1601,7 @@
|
|
| 1349 |
feedback.innerText = 'ACCESS GRANTED';
|
| 1350 |
input.classList.add('text-green-400');
|
| 1351 |
input.disabled = true;
|
|
|
|
| 1352 |
|
| 1353 |
setTimeout(() => {
|
| 1354 |
quizLayer.classList.add('hidden');
|
|
@@ -1369,13 +1622,33 @@
|
|
| 1369 |
}, 500);
|
| 1370 |
|
| 1371 |
// Show persistent wrong message
|
| 1372 |
-
feedback.className = 'text-red-400 text-lg font-bold';
|
| 1373 |
feedback.innerText = data.wrongMsg;
|
| 1374 |
}
|
| 1375 |
};
|
| 1376 |
|
| 1377 |
btn.onclick = checkAnswer;
|
| 1378 |
input.onkeypress = (e) => { if (e.key === 'Enter') checkAnswer(); };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
}
|
| 1380 |
|
| 1381 |
// Add shake animation style dynamically
|
|
|
|
| 195 |
pointer-events: none;
|
| 196 |
}
|
| 197 |
|
| 198 |
+
/* 平板/手機裝置:警探縮小並置於圖片後方 */
|
| 199 |
+
@media (max-width: 1024px),
|
| 200 |
+
(pointer: coarse) {
|
| 201 |
+
.character-sprite {
|
| 202 |
+
height: clamp(250px, 40vh, 450px);
|
| 203 |
+
z-index: 5;
|
| 204 |
+
/* 在其他圖片(z-index:10)之後 */
|
| 205 |
+
bottom: 60px;
|
| 206 |
+
left: -15px;
|
| 207 |
+
opacity: 0.85;
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
@media (max-width: 768px) {
|
| 212 |
+
.character-sprite {
|
| 213 |
+
height: clamp(180px, 30vh, 300px);
|
| 214 |
+
z-index: 5;
|
| 215 |
+
bottom: 50px;
|
| 216 |
+
left: -10px;
|
| 217 |
+
opacity: 0.75;
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
/* Scanlines */
|
| 222 |
.scanlines {
|
| 223 |
background: linear-gradient(to bottom,
|
|
|
|
| 270 |
flex-direction: column;
|
| 271 |
justify-content: flex-start;
|
| 272 |
}
|
| 273 |
+
|
| 274 |
+
/* ====== Virtual Keypad ====== */
|
| 275 |
+
#virtual-keypad {
|
| 276 |
+
position: fixed;
|
| 277 |
+
bottom: 20px;
|
| 278 |
+
right: 20px;
|
| 279 |
+
left: auto;
|
| 280 |
+
transform: translateY(120%);
|
| 281 |
+
z-index: 1000;
|
| 282 |
+
background: rgba(15, 5, 25, 0.95);
|
| 283 |
+
backdrop-filter: blur(10px);
|
| 284 |
+
-webkit-backdrop-filter: blur(10px);
|
| 285 |
+
border: 1px solid rgba(217, 70, 239, 0.4);
|
| 286 |
+
border-radius: 20px;
|
| 287 |
+
padding: 16px;
|
| 288 |
+
display: grid;
|
| 289 |
+
grid-template-columns: repeat(2, 1fr);
|
| 290 |
+
gap: 10px;
|
| 291 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(217, 70, 239, 0.15);
|
| 292 |
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 293 |
+
touch-action: none;
|
| 294 |
+
max-width: 200px;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
#virtual-keypad.active {
|
| 298 |
+
transform: translateY(0);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
#virtual-keypad.dragging {
|
| 302 |
+
transition: none;
|
| 303 |
+
transform: none;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.keypad-handle {
|
| 307 |
+
grid-column: span 2;
|
| 308 |
+
height: 20px;
|
| 309 |
+
margin-bottom: 4px;
|
| 310 |
+
background: rgba(255, 255, 255, 0.15);
|
| 311 |
+
border-radius: 10px;
|
| 312 |
+
cursor: grab;
|
| 313 |
+
display: flex;
|
| 314 |
+
justify-content: center;
|
| 315 |
+
align-items: center;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.keypad-handle::after {
|
| 319 |
+
content: '';
|
| 320 |
+
width: 40px;
|
| 321 |
+
height: 4px;
|
| 322 |
+
background: rgba(255, 255, 255, 0.35);
|
| 323 |
+
border-radius: 2px;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.keypad-btn {
|
| 327 |
+
width: 70px;
|
| 328 |
+
height: 60px;
|
| 329 |
+
border-radius: 12px;
|
| 330 |
+
background: rgba(30, 10, 50, 0.7);
|
| 331 |
+
border: 1px solid rgba(217, 70, 239, 0.25);
|
| 332 |
+
color: white;
|
| 333 |
+
font-size: 22px;
|
| 334 |
+
font-weight: bold;
|
| 335 |
+
font-family: 'Noto Sans TC', sans-serif;
|
| 336 |
+
display: flex;
|
| 337 |
+
align-items: center;
|
| 338 |
+
justify-content: center;
|
| 339 |
+
cursor: pointer;
|
| 340 |
+
transition: all 0.15s;
|
| 341 |
+
user-select: none;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.keypad-btn:active {
|
| 345 |
+
transform: scale(0.93);
|
| 346 |
+
background: rgba(217, 70, 239, 0.25);
|
| 347 |
+
border-color: rgba(217, 70, 239, 0.6);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.keypad-btn.action {
|
| 351 |
+
background: rgba(15, 5, 25, 0.8);
|
| 352 |
+
border-color: rgba(217, 70, 239, 0.5);
|
| 353 |
+
color: #d946ef;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.keypad-btn.submit {
|
| 357 |
+
background: rgba(217, 70, 239, 0.7);
|
| 358 |
+
color: white;
|
| 359 |
+
grid-column: span 2;
|
| 360 |
+
width: 100%;
|
| 361 |
+
height: 50px;
|
| 362 |
+
font-size: 16px;
|
| 363 |
+
margin-top: 4px;
|
| 364 |
+
letter-spacing: 2px;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.keypad-btn.submit:active {
|
| 368 |
+
background: rgba(217, 70, 239, 0.9);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
/* quiz-input 被選中時的高亮 */
|
| 372 |
+
#quiz-input.keypad-active {
|
| 373 |
+
border-color: #d946ef;
|
| 374 |
+
box-shadow: 0 0 12px rgba(217, 70, 239, 0.4);
|
| 375 |
+
}
|
| 376 |
</style>
|
| 377 |
</head>
|
| 378 |
|
|
|
|
| 440 |
</div>
|
| 441 |
</div>
|
| 442 |
|
| 443 |
+
<!-- Virtual Keypad HTML -->
|
| 444 |
+
<div id="virtual-keypad" onclick="event.stopPropagation()">
|
| 445 |
+
<div class="keypad-handle"></div>
|
| 446 |
+
<div class="keypad-btn" onclick="keypad.input('S')">S</div>
|
| 447 |
+
<div class="keypad-btn" onclick="keypad.input('A')">A</div>
|
| 448 |
+
<div class="keypad-btn" onclick="keypad.input('邊')">邊</div>
|
| 449 |
+
<div class="keypad-btn" onclick="keypad.input('角')">角</div>
|
| 450 |
+
<div class="keypad-btn action" onclick="keypad.backspace()">⌫</div>
|
| 451 |
+
<div class="keypad-btn action" onclick="keypad.clear()">清除</div>
|
| 452 |
+
<div class="keypad-btn submit" onclick="keypad.submit()">確認 ✓</div>
|
| 453 |
+
</div>
|
| 454 |
+
|
| 455 |
<script>
|
| 456 |
// Game Script
|
| 457 |
let playerName = localStorage.getItem('player_nickname') || '��鳥探員';
|
|
|
|
| 459 |
// Initial Script Placeholder - populated dynamically
|
| 460 |
let SCRIPT = [];
|
| 461 |
|
| 462 |
+
// ====== Virtual Keypad System ======
|
| 463 |
+
const keypad = {
|
| 464 |
+
element: null,
|
| 465 |
+
targetInput: null,
|
| 466 |
+
submitCallback: null,
|
| 467 |
+
init: function () {
|
| 468 |
+
this.element = document.getElementById('virtual-keypad');
|
| 469 |
+
if (!this.element) return;
|
| 470 |
+
|
| 471 |
+
// Prevent click bubbling
|
| 472 |
+
this.element.addEventListener('click', (e) => e.stopPropagation());
|
| 473 |
+
|
| 474 |
+
// Auto-close when clicking outside
|
| 475 |
+
document.addEventListener('click', (e) => {
|
| 476 |
+
if (this.element.classList.contains('active') &&
|
| 477 |
+
!this.element.contains(e.target) &&
|
| 478 |
+
e.target !== this.targetInput &&
|
| 479 |
+
!e.target.classList.contains('keypad-btn')) {
|
| 480 |
+
this.close();
|
| 481 |
+
}
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
// --- Drag Logic ---
|
| 485 |
+
const handle = this.element.querySelector('.keypad-handle');
|
| 486 |
+
this.isDragging = false;
|
| 487 |
+
this.offsetX = 0;
|
| 488 |
+
this.offsetY = 0;
|
| 489 |
+
|
| 490 |
+
const startDrag = (e) => {
|
| 491 |
+
if (e.type === 'touchstart' && e.cancelable) e.preventDefault();
|
| 492 |
+
this.isDragging = true;
|
| 493 |
+
this.element.classList.add('dragging');
|
| 494 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 495 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 496 |
+
const rect = this.element.getBoundingClientRect();
|
| 497 |
+
this.offsetX = clientX - rect.left;
|
| 498 |
+
this.offsetY = clientY - rect.top;
|
| 499 |
+
document.addEventListener(e.type.includes('mouse') ? 'mousemove' : 'touchmove', doDrag, { passive: false });
|
| 500 |
+
document.addEventListener(e.type.includes('mouse') ? 'mouseup' : 'touchend', endDrag);
|
| 501 |
+
};
|
| 502 |
+
|
| 503 |
+
const doDrag = (e) => {
|
| 504 |
+
if (!this.isDragging) return;
|
| 505 |
+
e.preventDefault();
|
| 506 |
+
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
|
| 507 |
+
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
|
| 508 |
+
this.element.style.transform = 'none';
|
| 509 |
+
this.element.style.bottom = 'auto';
|
| 510 |
+
this.element.style.right = 'auto';
|
| 511 |
+
this.element.style.left = (clientX - this.offsetX) + 'px';
|
| 512 |
+
this.element.style.top = (clientY - this.offsetY) + 'px';
|
| 513 |
+
};
|
| 514 |
+
|
| 515 |
+
const endDrag = (e) => {
|
| 516 |
+
this.isDragging = false;
|
| 517 |
+
this.element.classList.remove('dragging');
|
| 518 |
+
document.removeEventListener('mousemove', doDrag);
|
| 519 |
+
document.removeEventListener('touchmove', doDrag);
|
| 520 |
+
document.removeEventListener('mouseup', endDrag);
|
| 521 |
+
document.removeEventListener('touchend', endDrag);
|
| 522 |
+
};
|
| 523 |
+
|
| 524 |
+
if (handle) {
|
| 525 |
+
handle.addEventListener('mousedown', startDrag);
|
| 526 |
+
handle.addEventListener('touchstart', startDrag, { passive: false });
|
| 527 |
+
handle.style.cursor = 'grab';
|
| 528 |
+
}
|
| 529 |
+
},
|
| 530 |
+
open: function (inputElement, onSubmit) {
|
| 531 |
+
this.targetInput = inputElement;
|
| 532 |
+
this.submitCallback = onSubmit || null;
|
| 533 |
+
this.element.classList.add('active');
|
| 534 |
+
if (inputElement) {
|
| 535 |
+
inputElement.classList.add('keypad-active');
|
| 536 |
+
// 阻止系統鍵盤彈出
|
| 537 |
+
inputElement.setAttribute('readonly', 'readonly');
|
| 538 |
+
inputElement.setAttribute('inputmode', 'none');
|
| 539 |
+
}
|
| 540 |
+
},
|
| 541 |
+
close: function () {
|
| 542 |
+
this.element.classList.remove('active');
|
| 543 |
+
if (this.targetInput) {
|
| 544 |
+
this.targetInput.classList.remove('keypad-active');
|
| 545 |
+
this.targetInput.removeAttribute('readonly');
|
| 546 |
+
this.targetInput.removeAttribute('inputmode');
|
| 547 |
+
this.targetInput = null;
|
| 548 |
+
}
|
| 549 |
+
this.submitCallback = null;
|
| 550 |
+
},
|
| 551 |
+
input: function (val) {
|
| 552 |
+
if (!this.targetInput) return;
|
| 553 |
+
this.targetInput.value += val;
|
| 554 |
+
// 視覺回饋
|
| 555 |
+
this.targetInput.focus();
|
| 556 |
+
},
|
| 557 |
+
backspace: function () {
|
| 558 |
+
if (!this.targetInput) return;
|
| 559 |
+
this.targetInput.value = this.targetInput.value.slice(0, -1);
|
| 560 |
+
},
|
| 561 |
+
clear: function () {
|
| 562 |
+
if (!this.targetInput) return;
|
| 563 |
+
this.targetInput.value = '';
|
| 564 |
+
},
|
| 565 |
+
submit: function () {
|
| 566 |
+
if (this.submitCallback) {
|
| 567 |
+
this.submitCallback();
|
| 568 |
+
}
|
| 569 |
+
}
|
| 570 |
+
};
|
| 571 |
+
|
| 572 |
+
// 偵測是否為觸控裝置(平板/手機)
|
| 573 |
+
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
| 574 |
+
|
| 575 |
+
// Initialize keypad
|
| 576 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 577 |
+
keypad.init();
|
| 578 |
+
});
|
| 579 |
+
|
| 580 |
class GameEngine {
|
| 581 |
constructor() {
|
| 582 |
this.step = 0;
|
|
|
|
| 1576 |
|
| 1577 |
quizLayer.classList.remove('hidden');
|
| 1578 |
|
|
|
|
|
|
|
| 1579 |
// Integrated layout matching dialogue style exactly
|
| 1580 |
quizContent.innerHTML = `
|
| 1581 |
<div class="dialogue-name">資深警探 <span class="text-xs text-slate-500 ml-2 tracking-normal">// AUTHENTICATION REQUIRED</span></div>
|
| 1582 |
<div class="dialogue-text mb-4">${data.question}</div>
|
| 1583 |
|
| 1584 |
<div class="flex gap-4 w-full mt-auto items-end">
|
| 1585 |
+
<div class="text-fuchsia-500 font-tech text-xl animate-pulse">></div>
|
| 1586 |
+
<input type="text" id="quiz-input" class="flex-1 bg-transparent border-b-2 border-fuchsia-500/50 text-white text-xl p-2 focus:outline-none focus:border-fuchsia-400 font-mono tracking-wider placeholder-slate-600" placeholder="輸入答案..." autocomplete="off" inputmode="none">
|
| 1587 |
<button id="quiz-submit" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold px-8 py-2 rounded clip-path-slant text-lg font-tech tracking-widest border border-fuchsia-400/50 transition-all shadow-lg shadow-fuchsia-500/20">CONFIRM</button>
|
| 1588 |
</div>
|
| 1589 |
<div id="quiz-feedback" class="absolute top-6 right-8 font-tech text-xl font-bold"></div>
|
|
|
|
| 1593 |
const btn = document.getElementById('quiz-submit');
|
| 1594 |
const feedback = document.getElementById('quiz-feedback');
|
| 1595 |
|
|
|
|
|
|
|
| 1596 |
const checkAnswer = () => {
|
| 1597 |
const val = input.value.trim();
|
| 1598 |
if (data.answer.includes(val)) {
|
|
|
|
| 1601 |
feedback.innerText = 'ACCESS GRANTED';
|
| 1602 |
input.classList.add('text-green-400');
|
| 1603 |
input.disabled = true;
|
| 1604 |
+
keypad.close(); // 關閉小鍵盤
|
| 1605 |
|
| 1606 |
setTimeout(() => {
|
| 1607 |
quizLayer.classList.add('hidden');
|
|
|
|
| 1622 |
}, 500);
|
| 1623 |
|
| 1624 |
// Show persistent wrong message
|
| 1625 |
+
feedback.className = 'text-red-400 text-lg font-bold';
|
| 1626 |
feedback.innerText = data.wrongMsg;
|
| 1627 |
}
|
| 1628 |
};
|
| 1629 |
|
| 1630 |
btn.onclick = checkAnswer;
|
| 1631 |
input.onkeypress = (e) => { if (e.key === 'Enter') checkAnswer(); };
|
| 1632 |
+
|
| 1633 |
+
// 自動開啟小鍵盤(觸控裝置)或點擊輸入框時開啟
|
| 1634 |
+
if (isTouchDevice) {
|
| 1635 |
+
// 觸控裝置自動開啟
|
| 1636 |
+
setTimeout(() => {
|
| 1637 |
+
keypad.open(input, checkAnswer);
|
| 1638 |
+
}, 300);
|
| 1639 |
+
}
|
| 1640 |
+
|
| 1641 |
+
// 點擊輸入框也可以開啟小鍵盤
|
| 1642 |
+
input.addEventListener('click', (e) => {
|
| 1643 |
+
e.preventDefault();
|
| 1644 |
+
keypad.open(input, checkAnswer);
|
| 1645 |
+
});
|
| 1646 |
+
input.addEventListener('focus', (e) => {
|
| 1647 |
+
if (isTouchDevice) {
|
| 1648 |
+
e.preventDefault();
|
| 1649 |
+
keypad.open(input, checkAnswer);
|
| 1650 |
+
}
|
| 1651 |
+
});
|
| 1652 |
}
|
| 1653 |
|
| 1654 |
// Add shake animation style dynamically
|
skyscraper.html
CHANGED
|
@@ -798,21 +798,20 @@
|
|
| 798 |
});
|
| 799 |
const avg = totalErr / this.s.markerTs.length;
|
| 800 |
if (this.s.phase === 'tutorial') {
|
| 801 |
-
if (avg <
|
| 802 |
this._showPhaseModal('✅', '教學完成!', '垂直距離處處相等 = 平行!\n確認距離一致就代表兩條線平行', '確認,繼續 →', null);
|
| 803 |
this.dom.tbtn.onclick = () => { this.dom.tm.classList.add('hidden'); this.s.lvIdx++; this._genLevel() }
|
| 804 |
}
|
| 805 |
-
else this._showToast('還不夠精確
|
| 806 |
return;
|
| 807 |
}
|
| 808 |
if (this.s.fixMode) {
|
| 809 |
-
if (avg <
|
| 810 |
-
else
|
| 811 |
-
else this._showToast('還不夠精確,再調整!', false);
|
| 812 |
return;
|
| 813 |
}
|
| 814 |
-
if (avg <
|
| 815 |
-
else this._showToast('垂直距離不一致,再調整
|
| 816 |
}
|
| 817 |
|
| 818 |
judgeAnswer(says) {
|
|
@@ -905,24 +904,134 @@
|
|
| 905 |
if (this.s.playing) { this._drawBld(ctx, this.s.bL, 'left', f); this._drawBld(ctx, this.s.bR, 'right', f); this._drawScene(ctx, f) }
|
| 906 |
}
|
| 907 |
_drawBld(ctx, b, side, f) {
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
ctx.fillStyle =
|
| 911 |
-
ctx.
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
const
|
| 915 |
-
const
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 921 |
}
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
}
|
| 927 |
}
|
| 928 |
_drawScene(ctx, f) {
|
|
|
|
| 798 |
});
|
| 799 |
const avg = totalErr / this.s.markerTs.length;
|
| 800 |
if (this.s.phase === 'tutorial') {
|
| 801 |
+
if (avg < 2) {
|
| 802 |
this._showPhaseModal('✅', '教學完成!', '垂直距離處處相等 = 平行!\n確認距離一致就代表兩條線平行', '確認,繼續 →', null);
|
| 803 |
this.dom.tbtn.onclick = () => { this.dom.tm.classList.add('hidden'); this.s.lvIdx++; this._genLevel() }
|
| 804 |
}
|
| 805 |
+
else this._showToast('還不夠精確!各處的垂直距離必須完全相等才是平行', false);
|
| 806 |
return;
|
| 807 |
}
|
| 808 |
if (this.s.fixMode) {
|
| 809 |
+
if (avg < 1.5) { this._onCorrect(80) }
|
| 810 |
+
else this._showToast('還不夠精確!垂直距離必須完全一致才算平行', false);
|
|
|
|
| 811 |
return;
|
| 812 |
}
|
| 813 |
+
if (avg < 1.5) this._onCorrect(100);
|
| 814 |
+
else this._showToast('垂直距離不一致,還不是完全平行!再調整', false);
|
| 815 |
}
|
| 816 |
|
| 817 |
judgeAnswer(says) {
|
|
|
|
| 904 |
if (this.s.playing) { this._drawBld(ctx, this.s.bL, 'left', f); this._drawBld(ctx, this.s.bR, 'right', f); this._drawScene(ctx, f) }
|
| 905 |
}
|
| 906 |
_drawBld(ctx, b, side, f) {
|
| 907 |
+
// === 施工中建築 ===
|
| 908 |
+
// 鋼骨框架背景(半透明深色)
|
| 909 |
+
ctx.fillStyle = 'rgba(15, 40, 24, 0.4)';
|
| 910 |
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
| 911 |
+
|
| 912 |
+
// 垂直鋼骨柱(左右兩根主柱 + 中間支撐柱)
|
| 913 |
+
const steelColor = '#475569';
|
| 914 |
+
const steelHighlight = '#64748b';
|
| 915 |
+
const pillarW = 4;
|
| 916 |
+
ctx.fillStyle = steelColor;
|
| 917 |
+
ctx.fillRect(b.x, b.y, pillarW, b.h); // 左柱
|
| 918 |
+
ctx.fillRect(b.x + b.w - pillarW, b.y, pillarW, b.h); // 右柱
|
| 919 |
+
ctx.fillRect(b.x + b.w / 2 - pillarW / 2, b.y, pillarW, b.h); // 中柱
|
| 920 |
+
|
| 921 |
+
// 水平鋼骨樓板(每隔一段距離一條)
|
| 922 |
+
const floorGap = Math.max(30, b.h / 10);
|
| 923 |
+
const numFloors = Math.floor(b.h / floorGap);
|
| 924 |
+
for (let i = 0; i <= numFloors; i++) {
|
| 925 |
+
const fy = b.y + i * floorGap;
|
| 926 |
+
if (fy > b.y + b.h) break;
|
| 927 |
+
ctx.fillStyle = steelColor;
|
| 928 |
+
ctx.fillRect(b.x, fy, b.w, 3);
|
| 929 |
+
// 高亮線
|
| 930 |
+
ctx.fillStyle = steelHighlight;
|
| 931 |
+
ctx.fillRect(b.x, fy, b.w, 1);
|
| 932 |
}
|
| 933 |
+
|
| 934 |
+
// 交叉鋼骨支撐(X 型斜撐,每隔一層交替方向)
|
| 935 |
+
ctx.strokeStyle = 'rgba(100, 116, 139, 0.5)';
|
| 936 |
+
ctx.lineWidth = 1.5;
|
| 937 |
+
for (let i = 0; i < numFloors; i++) {
|
| 938 |
+
const y1 = b.y + i * floorGap;
|
| 939 |
+
const y2 = Math.min(b.y + (i + 1) * floorGap, b.y + b.h);
|
| 940 |
+
// 左半 X
|
| 941 |
+
if (i % 2 === 0) {
|
| 942 |
+
ctx.beginPath();
|
| 943 |
+
ctx.moveTo(b.x, y1); ctx.lineTo(b.x + b.w / 2, y2);
|
| 944 |
+
ctx.stroke();
|
| 945 |
+
ctx.beginPath();
|
| 946 |
+
ctx.moveTo(b.x + b.w / 2, y1); ctx.lineTo(b.x, y2);
|
| 947 |
+
ctx.stroke();
|
| 948 |
+
} else {
|
| 949 |
+
// 右半 X
|
| 950 |
+
ctx.beginPath();
|
| 951 |
+
ctx.moveTo(b.x + b.w / 2, y1); ctx.lineTo(b.x + b.w, y2);
|
| 952 |
+
ctx.stroke();
|
| 953 |
+
ctx.beginPath();
|
| 954 |
+
ctx.moveTo(b.x + b.w, y1); ctx.lineTo(b.x + b.w / 2, y2);
|
| 955 |
+
ctx.stroke();
|
| 956 |
+
}
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
// 鷹架(外側細格線)
|
| 960 |
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.2)';
|
| 961 |
+
ctx.lineWidth = 1;
|
| 962 |
+
const scaffoldGap = 15;
|
| 963 |
+
for (let sy = b.y; sy < b.y + b.h; sy += scaffoldGap) {
|
| 964 |
+
ctx.beginPath();
|
| 965 |
+
ctx.moveTo(b.x - 6, sy); ctx.lineTo(b.x, sy);
|
| 966 |
+
ctx.stroke();
|
| 967 |
+
ctx.beginPath();
|
| 968 |
+
ctx.moveTo(b.x + b.w, sy); ctx.lineTo(b.x + b.w + 6, sy);
|
| 969 |
+
ctx.stroke();
|
| 970 |
+
}
|
| 971 |
+
// 鷹架垂直線
|
| 972 |
+
ctx.beginPath(); ctx.moveTo(b.x - 6, b.y); ctx.lineTo(b.x - 6, b.y + b.h); ctx.stroke();
|
| 973 |
+
ctx.beginPath(); ctx.moveTo(b.x + b.w + 6, b.y); ctx.lineTo(b.x + b.w + 6, b.y + b.h); ctx.stroke();
|
| 974 |
+
|
| 975 |
+
// 安全網(頂部附近,半透明綠色區域)
|
| 976 |
+
const netH = Math.min(floorGap * 2, b.h * 0.2);
|
| 977 |
+
ctx.fillStyle = 'rgba(34, 197, 94, 0.06)';
|
| 978 |
+
ctx.fillRect(b.x, b.y, b.w, netH);
|
| 979 |
+
// 安全網格線
|
| 980 |
+
ctx.strokeStyle = 'rgba(34, 197, 94, 0.15)';
|
| 981 |
+
ctx.lineWidth = 0.5;
|
| 982 |
+
for (let nx = b.x; nx < b.x + b.w; nx += 8) {
|
| 983 |
+
ctx.beginPath(); ctx.moveTo(nx, b.y); ctx.lineTo(nx, b.y + netH); ctx.stroke();
|
| 984 |
+
}
|
| 985 |
+
for (let ny = b.y; ny < b.y + netH; ny += 8) {
|
| 986 |
+
ctx.beginPath(); ctx.moveTo(b.x, ny); ctx.lineTo(b.x + b.w, ny); ctx.stroke();
|
| 987 |
+
}
|
| 988 |
+
|
| 989 |
+
// 未完成頂部(鋸齒狀不規則頂端)
|
| 990 |
+
ctx.fillStyle = steelColor;
|
| 991 |
+
const toothW = b.w / 5;
|
| 992 |
+
for (let tx = b.x; tx < b.x + b.w; tx += toothW) {
|
| 993 |
+
const th = 5 + Math.abs(Math.sin(tx * 0.7)) * 12;
|
| 994 |
+
ctx.fillRect(tx, b.y - th, Math.min(pillarW, toothW), th);
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
// 起重機塔吊
|
| 998 |
+
const craneX = side === 'left' ? b.x + b.w * 0.3 : b.x + b.w * 0.7;
|
| 999 |
+
const craneTopY = b.y - 40;
|
| 1000 |
+
// 塔身
|
| 1001 |
+
ctx.strokeStyle = '#f97316';
|
| 1002 |
+
ctx.lineWidth = 3;
|
| 1003 |
+
ctx.beginPath(); ctx.moveTo(craneX, b.y); ctx.lineTo(craneX, craneTopY); ctx.stroke();
|
| 1004 |
+
// 吊臂(水平)
|
| 1005 |
+
const armLen = side === 'left' ? b.w * 1.2 : -b.w * 1.2;
|
| 1006 |
+
ctx.lineWidth = 2;
|
| 1007 |
+
ctx.beginPath(); ctx.moveTo(craneX, craneTopY); ctx.lineTo(craneX + armLen, craneTopY); ctx.stroke();
|
| 1008 |
+
// 支撐鋼索
|
| 1009 |
+
ctx.strokeStyle = 'rgba(249, 115, 22, 0.4)'; ctx.lineWidth = 1;
|
| 1010 |
+
ctx.beginPath(); ctx.moveTo(craneX, craneTopY - 8); ctx.lineTo(craneX + armLen * 0.7, craneTopY); ctx.stroke();
|
| 1011 |
+
ctx.beginPath(); ctx.moveTo(craneX, craneTopY - 8); ctx.lineTo(craneX + armLen * 0.3, craneTopY); ctx.stroke();
|
| 1012 |
+
// 吊索 + 吊鉤(動態擺動)
|
| 1013 |
+
const hookX = craneX + armLen * (0.5 + Math.sin(f * 0.02) * 0.15);
|
| 1014 |
+
const hookY = craneTopY + 20 + Math.sin(f * 0.03) * 5;
|
| 1015 |
+
ctx.strokeStyle = 'rgba(249, 115, 22, 0.6)'; ctx.lineWidth = 1;
|
| 1016 |
+
ctx.beginPath(); ctx.moveTo(hookX, craneTopY); ctx.lineTo(hookX, hookY); ctx.stroke();
|
| 1017 |
+
// 吊鉤
|
| 1018 |
+
ctx.fillStyle = '#f97316';
|
| 1019 |
+
ctx.beginPath(); ctx.arc(hookX, hookY, 3, 0, Math.PI * 2); ctx.fill();
|
| 1020 |
+
ctx.beginPath(); ctx.arc(hookX, hookY + 5, 5, 0, Math.PI); ctx.stroke();
|
| 1021 |
+
|
| 1022 |
+
// 閃爍警示燈(頂部)
|
| 1023 |
+
if (Math.sin(f * .06) > 0) {
|
| 1024 |
+
ctx.beginPath(); ctx.arc(craneX, craneTopY - 8, 3, 0, Math.PI * 2);
|
| 1025 |
+
ctx.fillStyle = '#ef4444'; ctx.shadowBlur = 10; ctx.shadowColor = '#ef4444'; ctx.fill(); ctx.shadowBlur = 0;
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
// 底部建材堆放(矩形鋼筋堆)
|
| 1029 |
+
ctx.fillStyle = 'rgba(148, 163, 184, 0.3)';
|
| 1030 |
+
const stackY = b.y + b.h - 15;
|
| 1031 |
+
for (let sx = 0; sx < 3; sx++) {
|
| 1032 |
+
ctx.fillRect(b.x + 8 + sx * (b.w / 4), stackY, b.w / 5, 10);
|
| 1033 |
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.5)'; ctx.lineWidth = 0.5;
|
| 1034 |
+
ctx.strokeRect(b.x + 8 + sx * (b.w / 4), stackY, b.w / 5, 10);
|
| 1035 |
}
|
| 1036 |
}
|
| 1037 |
_drawScene(ctx, f) {
|