802MathCity / congruence_detective.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, viewport-fit=cover">
<title>全等重案組 - Congruence Unit</title>
<script src="https://cdn.tailwindcss.com"></script>
<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">
<style>
body {
font-family: 'Noto Sans TC', sans-serif;
background-color: #050510;
overflow: hidden;
color: white;
touch-action: none;
user-select: none;
}
.font-tech {
font-family: 'Orbitron', sans-serif;
}
/* Glassmorphism Panel */
.glass-panel {
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(147, 51, 234, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
/* Cyberpunk Dialogue Box */
.dialogue-box {
position: absolute;
bottom: 20px;
left: 5%;
width: 90%;
height: 220px;
/* Increased height */
background: rgba(0, 0, 0, 0.9);
border: 2px solid #d946ef;
/* Magenta */
border-left-width: 8px;
box-shadow: 0 0 20px rgba(217, 70, 239, 0.3);
padding: 24px;
/* Increased padding */
z-index: 50;
display: flex;
flex-direction: column;
justify-content: flex-start;
pointer-events: auto;
clip-path: polygon(0 0,
100% 0,
100% 85%,
95% 100%,
0% 100%);
}
.dialogue-name {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
/* Increased size */
color: #d946ef;
font-weight: bold;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 10px rgba(217, 70, 239, 0.5);
}
.dialogue-text {
font-size: 1.3rem;
/* Increased size */
line-height: 1.6;
color: #e2e8f0;
font-weight: 500;
}
/* ... cursor blink styles ... */
.cursor-blink::after {
content: '▋';
display: inline-block;
animation: blink 1s infinite;
color: #d946ef;
margin-left: 5px;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
/* Hitbox Styles */
.hitbox {
position: absolute;
width: 60px;
height: 60px;
border-radius: 50%;
background: rgba(250, 204, 21, 0.25);
/* Yellow/Amber tint */
border: 2px dashed rgba(250, 204, 21, 0.8);
cursor: pointer;
z-index: 100;
transition: all 0.2s;
animation: pulse-ring 2s infinite;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: 900;
font-size: 1.8rem;
text-shadow: 0 0 5px black;
}
.hitbox.found-angle {
background: rgba(239, 68, 68, 0.4);
/* Red */
border: 2px solid #ef4444;
animation: none;
pointer-events: none;
}
.hitbox.found-side {
background: rgba(59, 130, 246, 0.4);
/* Blue */
border: 2px solid #3b82f6;
animation: none;
pointer-events: none;
}
.hitbox:hover {
transform: scale(1.1);
border-color: #fef08a;
/* Lighter yellow on hover */
background: rgba(250, 204, 21, 0.4);
}
@keyframes pulse-ring {
0% {
box-shadow: 0 0 0 0 rgba(250, 204, 21, 0.6);
}
70% {
box-shadow: 0 0 0 15px rgba(250, 204, 21, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(250, 204, 21, 0);
}
}
.evidence-counter {
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #d946ef;
padding: 10px 20px;
border-radius: 8px;
z-index: 100;
font-family: 'Orbitron';
color: #d946ef;
}
.character-sprite {
position: absolute;
bottom: 80px;
/* Moved up significantly - hidden behind dialogue box */
left: -20px;
/* Adjusted position */
height: clamp(600px, 100vh, 1200px);
/* Even larger and higher */
z-index: 40;
/* Behind dialogue box */
filter: drop-shadow(0 0 20px rgba(217, 70, 239, 0.4));
transition: all 0.3s ease-out;
pointer-events: none;
}
.character-sprite.hidden {
opacity: 0;
transform: translateX(-50px);
pointer-events: none;
}
/* 平板/手機裝置:警探縮小並置於圖片後方 */
@media (max-width: 1024px),
(pointer: coarse) {
.character-sprite {
height: clamp(250px, 40vh, 450px);
z-index: 5;
/* 在其他圖片(z-index:10)之後 */
bottom: 60px;
left: -15px;
opacity: 0.85;
}
}
@media (max-width: 768px) {
.character-sprite {
height: clamp(180px, 30vh, 300px);
z-index: 5;
bottom: 50px;
left: -10px;
opacity: 0.75;
}
}
/* Scanlines */
.scanlines {
background: linear-gradient(to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.1));
background-size: 100% 4px;
position: fixed;
inset: 0;
pointer-events: none;
z-index: 200;
mix-blend-mode: overlay;
}
/* Quiz Layer - Fixed Fullscreen Overlay */
#quiz-layer {
position: fixed;
inset: 0;
z-index: 150;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 40%, rgba(0, 0, 0, 0.8) 100%);
/* Gradient mask to keep top clear */
backdrop-filter: blur(1px);
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 40px;
/* Lift slightly higher than dialogue */
pointer-events: auto;
}
#quiz-layer.hidden {
display: none !important;
pointer-events: none;
}
#quiz-layer .quiz-box {
position: relative;
width: 90%;
max-width: 1200px;
height: 250px;
background: rgba(5, 5, 10, 0.95);
border: 2px solid #d946ef;
border-left-width: 8px;
box-shadow: 0 0 30px rgba(217, 70, 239, 0.4);
pointer-events: auto;
clip-path: polygon(0 0, 100% 0, 100% 85%, 95% 100%, 0% 100%);
padding: 30px;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
/* ====== Virtual Keypad ====== */
#virtual-keypad {
position: fixed;
bottom: 20px;
right: 20px;
left: auto;
transform: translateY(120%);
z-index: 1000;
background: rgba(15, 5, 25, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(217, 70, 239, 0.4);
border-radius: 20px;
padding: 16px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(217, 70, 239, 0.15);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
touch-action: none;
max-width: 200px;
}
#virtual-keypad.active {
transform: translateY(0);
}
#virtual-keypad.dragging {
transition: none;
transform: none;
}
.keypad-handle {
grid-column: span 2;
height: 20px;
margin-bottom: 4px;
background: rgba(255, 255, 255, 0.15);
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.35);
border-radius: 2px;
}
.keypad-btn {
width: 70px;
height: 60px;
border-radius: 12px;
background: rgba(30, 10, 50, 0.7);
border: 1px solid rgba(217, 70, 239, 0.25);
color: white;
font-size: 22px;
font-weight: bold;
font-family: 'Noto Sans TC', sans-serif;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.keypad-btn:active {
transform: scale(0.93);
background: rgba(217, 70, 239, 0.25);
border-color: rgba(217, 70, 239, 0.6);
}
.keypad-btn.action {
background: rgba(15, 5, 25, 0.8);
border-color: rgba(217, 70, 239, 0.5);
color: #d946ef;
}
.keypad-btn.submit {
background: rgba(217, 70, 239, 0.7);
color: white;
grid-column: span 2;
width: 100%;
height: 50px;
font-size: 16px;
margin-top: 4px;
letter-spacing: 2px;
}
.keypad-btn.submit:active {
background: rgba(217, 70, 239, 0.9);
}
/* quiz-input 被選中時的高亮 */
#quiz-input.keypad-active {
border-color: #d946ef;
box-shadow: 0 0 12px rgba(217, 70, 239, 0.4);
}
</style>
</head>
<body class="bg-slate-900 text-white selection:bg-fuchsia-500 selection:text-white">
<div class="scanlines"></div>
<!-- Background - Static for now, can be dynamic -->
<div class="fixed inset-0 z-0 bg-cover bg-center opacity-40"
style="background-image: url('Assets/index/indexbg.png'); filter: blur(4px) hue-rotate(45deg);"></div>
<!-- Game Container -->
<div id="game-stage" class="relative w-full h-full min-h-screen overflow-hidden flex flex-col">
<!-- Header -->
<div class="absolute top-0 left-0 w-full p-4 z-50 flex justify-between items-start pointer-events-none">
<div class="glass-panel px-6 py-2 rounded-br-2xl border-l-4 border-l-fuchsia-500 pointer-events-auto">
<h1 class="text-xl font-bold text-fuchsia-400 tracking-wider">全等重案組<br><span
class="font-tech text-sm">CONGRUENCE UNIT</span></h1>
<div class="text-xs text-slate-400 font-mono mt-1">CASE FILE #001: THE STYLIST</div>
</div>
<a href="index.html"
class="glass-panel rounded-xl px-3 py-2 flex items-center justify-center text-amber-400 hover:bg-white/10 transition-all pointer-events-auto shadow-lg border border-amber-500/30 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>
<!-- Evidence/Status Display (Hidden by default) -->
<div id="evidence-panel" class="hidden">
<div class="evidence-counter">
<div class="text-xs text-secondary mb-1">EVIDENCE COLLECTED</div>
<div class="text-2xl font-bold" id="evidence-count">0 / 6</div>
</div>
</div>
<!-- Main Interaction Area -->
<div id="scene-layer" class="relative flex-1 z-10 flex items-center justify-center">
<!-- Target Container for Investigation -->
<div id="target-container" class="flex-1 flex justify-center items-center relative p-4 hidden">
<div id="suspect-wrapper" class="relative inline-block">
<img id="suspect-image" src="Assets/triangle/嫌疑犯/16造型師.png" alt="Suspect"
class="max-h-[60vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700">
</div>
</div> <!-- Hitboxes will be appended here -->
</div>
<!-- Dialogue Layer -->
<div id="dialogue-layer" class="hidden" onclick="window.game.next()">
<img id="speaker-sprite" src="" class="character-sprite hidden" alt="Character">
<div class="dialogue-box cursor-pointer hover:bg-black/95 transition-colors">
<div id="speaker-name" class="dialogue-name">SENIOR DETECTIVE</div>
<div id="dialogue-text" class="dialogue-text cursor-blink"></div>
<div class="absolute bottom-4 right-4 text-xs text-fuchsia-500 animate-bounce">▼ CLICK TO CONTINUE
</div>
</div>
</div>
<!-- Quiz Layer (Modified) -->
<div id="quiz-layer" class="hidden">
<div class="quiz-box">
<h2 class="text-lg font-bold text-fuchsia-400 font-tech mb-1">SYSTEM AUTHENTICATION</h2>
<div id="quiz-content" class="w-full"></div>
</div>
</div>
</div>
<!-- Virtual Keypad HTML -->
<div id="virtual-keypad" onclick="event.stopPropagation()">
<div class="keypad-handle"></div>
<div class="keypad-btn" onclick="keypad.input('S')">S</div>
<div class="keypad-btn" onclick="keypad.input('A')">A</div>
<div class="keypad-btn" onclick="keypad.input('邊')"></div>
<div class="keypad-btn" onclick="keypad.input('角')"></div>
<div class="keypad-btn action" onclick="keypad.backspace()"></div>
<div class="keypad-btn action" onclick="keypad.clear()">清除</div>
<div class="keypad-btn submit" onclick="keypad.submit()">確認 ✓</div>
</div>
<!-- 水印 -->
<div id="watermark"
style="position:fixed;bottom:4px;right:8px;text-align:right;font-size:10px;color:rgba(148,163,184,.35);z-index:9998;pointer-events:none;font-family:'Noto Sans TC',sans-serif;line-height:1.5">
<div>程式設計者:新竹縣精華國中 藍星宇</div>
<div>FB教育社團:<a href="https://www.facebook.com/groups/1554372228718393" target="_blank"
style="color:rgba(148,163,184,.5);pointer-events:auto;text-decoration:none;transition:color .2s"
onmouseover="this.style.color='#d946ef'" onmouseout="this.style.color='rgba(148,163,184,.5)'">萬物皆數</a>
</div>
</div>
<script>
// Game Script
let playerName = localStorage.getItem('player_nickname') || '菜鳥探員';
// Initial Script Placeholder - populated dynamically
let SCRIPT = [];
// ====== Virtual Keypad System ======
const keypad = {
element: null,
targetInput: null,
submitCallback: null,
init: function () {
this.element = document.getElementById('virtual-keypad');
if (!this.element) return;
// Prevent click bubbling
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.offsetX = 0;
this.offsetY = 0;
const startDrag = (e) => {
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;
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();
const clientX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
const clientY = e.type.includes('mouse') ? e.clientY : e.touches[0].clientY;
this.element.style.transform = 'none';
this.element.style.bottom = 'auto';
this.element.style.right = 'auto';
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);
};
if (handle) {
handle.addEventListener('mousedown', startDrag);
handle.addEventListener('touchstart', startDrag, { passive: false });
handle.style.cursor = 'grab';
}
},
open: function (inputElement, onSubmit) {
this.targetInput = inputElement;
this.submitCallback = onSubmit || null;
this.element.classList.add('active');
if (inputElement) {
inputElement.classList.add('keypad-active');
// 阻止系統鍵盤彈出
inputElement.setAttribute('readonly', 'readonly');
inputElement.setAttribute('inputmode', 'none');
}
},
close: function () {
this.element.classList.remove('active');
if (this.targetInput) {
this.targetInput.classList.remove('keypad-active');
this.targetInput.removeAttribute('readonly');
this.targetInput.removeAttribute('inputmode');
this.targetInput = null;
}
this.submitCallback = null;
},
input: function (val) {
if (!this.targetInput) return;
this.targetInput.value += val;
// 視覺回饋
this.targetInput.focus();
},
backspace: function () {
if (!this.targetInput) return;
this.targetInput.value = this.targetInput.value.slice(0, -1);
},
clear: function () {
if (!this.targetInput) return;
this.targetInput.value = '';
},
submit: function () {
if (this.submitCallback) {
this.submitCallback();
}
}
};
// 偵測是否為觸控裝置(平板/手機)
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
// Initialize keypad
document.addEventListener('DOMContentLoaded', () => {
keypad.init();
});
class GameEngine {
constructor() {
this.step = 0;
this.isTyping = false;
this.elDialogue = document.getElementById('dialogue-layer');
this.elText = document.getElementById('dialogue-text');
this.elName = document.getElementById('speaker-name');
this.elScene = document.getElementById('scene-layer');
this.elTarget = document.getElementById('target-container');
this.foundFeatures = new Set();
this.angleCount = 0;
this.sideCount = 0;
this.requiredFeatures = 6;
this.init();
}
init() {
this.addStyles();
this.setupInitialScript();
this.showDialogue(true);
this.processStep();
// 全螢幕:需要使用者互動事件才能觸發,所以掛在第一次點擊上
this._fullscreenOnce = false;
const tryFullscreen = () => {
if (this._fullscreenOnce) return;
this._fullscreenOnce = true;
try {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen().catch(e => console.log('Fullscreen denied:', e));
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
}
} catch (err) { console.log('Fullscreen not supported'); }
document.removeEventListener('click', tryFullscreen);
document.removeEventListener('touchstart', tryFullscreen);
};
document.addEventListener('click', tryFullscreen, { once: true });
document.addEventListener('touchstart', tryFullscreen, { once: true });
}
setupInitialScript() {
// Formatting helper
const fmtName = (n) => `<span class="text-amber-400 font-bold mx-1 text-xl">${n}</span>`;
SCRIPT = [
{
type: 'dialogue',
speaker: '資深警探',
text: '歡迎來到 Math City 的重案組,我是負責帶領你的前輩,請問你怎麼稱呼?',
},
{
type: 'input_name',
speaker: 'SYSTEM',
text: '請輸入你的暱稱...'
},
{
type: 'dialogue',
speaker: '資深警探',
text: (name) => `${fmtName(name)}!真是個不錯的名字呢,時間緊迫,我就有話直說了。`
},
{
type: 'dialogue',
speaker: '資深警探',
text: '雖然城裡看似和平,但最近發生了一連串的案件,我們需要你的協助!'
},
{
type: 'dialogue',
speaker: '資深警探',
text: (name) => `對於這些犯罪,我們已經掌握了一部分的證據,但是緝凶人手不足,${fmtName(name)}看起來很聰明,我們需要你的幫忙。`
},
{
type: 'dialogue',
speaker: '資深警探',
text: '在三角形中,就像你臉上的<span class="text-yellow-400 font-bold">鼻子</span>、<span class="text-yellow-400 font-bold">嘴巴</span>、<span class="text-yellow-400 font-bold">眼睛</span>一樣,也有獨特的<span class="text-yellow-400 font-bold">特徵</span>,你知道是哪些嗎?'
},
{
type: 'choice',
question: '你知道三角形的特徵嗎?',
options: [
{ text: '我知道!', next: 'next_step' },
{ text: '不知道...', next: 'next_step' }
]
},
{
type: 'action',
action: 'show_suspect_investigation',
speaker: '資深警探',
text: '現在我們來練習一下。看看這張照片,這是一位嫌疑犯,請你找出他的<span class="text-yellow-400 font-bold">特徵點</span>。<br>三角形的特徵點就是所有的邊和角,<span class="text-green-400 font-bold animate-pulse mt-2 mb-2 inline-block bg-slate-800/80 px-3 py-1.5 rounded-lg border-2 border-green-500/60 shadow-[0_0_10px_rgba(34,197,94,0.3)]">👉 請「直接點擊」上方圖片中</span> 的 <span class="text-yellow-400 font-bold">3 個邊</span> 與 <span class="text-yellow-400 font-bold">3 個角</span>!<br><span id="stats-display" class="text-yellow-400 font-mono block text-lg"></span>'
}
];
}
next() {
if (this.isTyping) {
this.finishTyping();
return;
}
if (SCRIPT[this.step] && (SCRIPT[this.step].type === 'action' || SCRIPT[this.step].type === 'input_name' || SCRIPT[this.step].type === 'choice') && !this.actionCompleted) {
return;
}
this.step++;
if (this.step < SCRIPT.length) {
this.processStep();
} else {
console.log('Script Finished');
}
}
processStep() {
const data = SCRIPT[this.step];
let text = typeof data.text === 'function' ? data.text(playerName) : data.text;
if (data.type === 'dialogue') {
this.showDialogue(true);
this.setSpeaker(data.speaker);
this.typeText(text);
this.actionCompleted = true; // Auto-complete purely dialogue steps? No, wait for click.
this.actionCompleted = true; // Actually previous logic relied on next() call, which checks actionCompleted only for actions.
// My logic in next() was: if type is action/input/choice and !actionCompleted, return.
// For dialogue, we don't block.
} else if (data.type === 'action') {
this.showDialogue(true);
this.setSpeaker(data.speaker);
this.typeText(text);
this.handleAction(data.action);
} else if (data.type === 'input_name') {
this.showDialogue(true);
this.setSpeaker(data.speaker);
this.typeText(text);
this.showNameInput();
} else if (data.type === 'choice') {
this.showDialogue(true); // Keep dialogue visible asking question? Or hide?
// Usually hide for choice or overlay. Let's overlay.
this.showChoices(data.options);
} else if (data.type === 'quiz') {
this.showDialogue(false);
this.showQuiz(data);
}
}
setSpeaker(name) {
this.elName.innerText = name;
const sprite = document.getElementById('speaker-sprite');
if (name === '資深警探') {
sprite.src = 'Assets/triangle/detective.svg'; // Use our new SVG
sprite.classList.remove('hidden');
// Add some animation or style if needed
sprite.style.filter = "drop-shadow(0 0 10px #d946ef)";
} else if (name === 'SYSTEM') {
sprite.classList.add('hidden');
} else {
// Default behavior for other speakers
sprite.classList.add('hidden');
}
}
typeText(text) {
this.isTyping = true;
this.elText.innerHTML = '';
this.fullText = text;
let i = 0;
clearInterval(this.typeInterval);
this.typeInterval = setInterval(() => {
this.elText.innerHTML = this.fullText.substring(0, i + 1);
i++;
if (i === this.fullText.length) {
this.finishTyping();
}
}, 30);
}
finishTyping() {
clearInterval(this.typeInterval);
this.elText.innerHTML = this.fullText;
this.isTyping = false;
// If it's the investigation step, ensure stats are updated
if (SCRIPT[this.step] && SCRIPT[this.step].action === 'show_suspect_investigation') {
this.updateStatsDisplay();
}
}
showDialogue(show) {
if (show) this.elDialogue.classList.remove('hidden');
else this.elDialogue.classList.add('hidden');
}
// Input Name Logic
showNameInput() {
this.actionCompleted = false;
const inputHtml = `
<div id="name-input-overlay" class="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 w-full max-w-md">
<h2 class="text-xl font-bold text-fuchsia-400 text-center">輸入代號 INPUT ID</h2>
<input type="text" id="player-name-input" class="bg-slate-900 border border-fuchsia-500 rounded p-3 text-white text-center text-xl focus:outline-none" placeholder="你的暱稱" maxlength="10">
<button id="name-submit-btn" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-3 rounded transition-colors">確認 CONFIRM</button>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', inputHtml);
const btn = document.getElementById('name-submit-btn');
const input = document.getElementById('player-name-input');
input.focus();
const submit = () => {
const val = input.value.trim();
if (val) {
playerName = val;
localStorage.setItem('player_nickname', playerName);
document.getElementById('name-input-overlay').remove();
this.actionCompleted = true;
this.next();
}
};
btn.onclick = submit;
input.onkeypress = (e) => { if (e.key === 'Enter') submit(); };
}
// Choice Logic
showChoices(options) {
this.actionCompleted = false;
const choiceContainer = document.createElement('div');
choiceContainer.id = 'choice-overlay';
choiceContainer.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/60 backdrop-blur-sm';
let html = '<div class="flex flex-col gap-4 min-w-[300px]">';
options.forEach((opt, idx) => {
html += `<button class="choice-btn glass-panel px-6 py-4 rounded-lg text-xl font-bold text-white hover:bg-fuchsia-600 transition-colors border-l-4 border-fuchsia-400 hover:scale-105 transform duration-200" data-idx="${idx}">${opt.text}</button>`;
});
html += '</div>';
choiceContainer.innerHTML = html;
document.body.appendChild(choiceContainer);
const btns = choiceContainer.querySelectorAll('.choice-btn');
btns.forEach(btn => {
btn.onclick = () => {
document.getElementById('choice-overlay').remove();
this.actionCompleted = true;
this.next();
};
});
}
// ====== CASE DATA ======
caseData = {
case1: {
id: 1,
title: '霓虹暗巷重傷害案',
titleEn: 'NEON ALLEY ASSAULT',
caseImage: 'Assets/triangle/Case/case1.png',
description: '凌晨兩點,幾何酒吧後巷發生了一起惡意鬥毆。犯人使用自己三角形的頭部作為兇器,給了被害人致命一擊,甚至把巷口的鈦合金垃圾桶撞出了一個深深的凹痕。金屬凹痕與路口監視器留下了關鍵的蛛絲馬跡。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
evidenceImage: 'Assets/triangle/蛛絲馬跡/12.png',
suspects: [
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/12教宗.png', name: '教宗' },
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/2農夫.png', name: '農夫' },
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/13軍人.png', name: '軍人' }
],
correctSuspect: 'suspect_1',
correctMethod: 'SSS',
nextCase: 'case2'
},
case2: {
id: 2,
title: '頂級俱樂部入侵案',
titleEn: 'ELITE CLUB INTRUSION',
caseImage: 'Assets/triangle/Case/case2.png',
description: '只容許上流社會進入的「等腰俱樂部」昨晚遭人強行破壞闖入。犯人沒有用炸藥,而是利用自己天生如利刃般鋒利的「頭頂」,加熱後直接在防彈門上熔出了一個缺口。安檢門的破壞痕跡與監視攝影機拍到了蛛絲馬跡的內容。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
evidenceImage: 'Assets/triangle/蛛絲馬跡/3.png',
suspects: [
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/5冰淇淋師傅.png', name: '冰淇淋師傅' },
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/8列車長.png', name: '列車長' },
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/11醫生.png', name: '醫生' }
],
correctSuspect: 'suspect_2',
correctMethod: 'SAS',
nextCase: 'case3'
},
case3: {
id: 3,
title: '市政廳塗鴉案',
titleEn: 'CITY HALL GRAFFITI',
caseImage: 'Assets/triangle/Case/case3.png',
description: '街頭塗鴉客「幻影角」昨夜觸發了市政廳的隱藏防盜陷阱——「瞬間定型漆」。在他正貼著牆面作畫時,防盜漆瞬間噴發,雖然他及時逃脫,但牆面上留下了一個完美的「三角形空白剪影」。牆上的空白輪廓與警用無人機拍到了蛛絲馬跡的內容。警方已經鎖定了三位嫌疑犯,請你證明哪一位是犯人,並且說明證明方法!',
evidenceImage: 'Assets/triangle/蛛絲馬跡/17.png',
suspects: [
{ id: 'suspect_1', src: 'Assets/triangle/嫌疑犯/9_DJ.png', name: 'DJ' },
{ id: 'suspect_2', src: 'Assets/triangle/嫌疑犯/13軍人.png', name: '軍人' },
{ id: 'suspect_3', src: 'Assets/triangle/嫌疑犯/11醫生.png', name: '醫生' }
],
correctSuspect: 'suspect_3',
correctMethod: 'SAS',
nextCase: null
}
};
handleAction(actionName) {
this.actionCompleted = false;
if (actionName === 'show_suspect_investigation') {
this.startInvestigation();
} else if (actionName === 'show_sss_sas_images') {
this.showSSSSASImages();
} else if (actionName.startsWith('show_practice_')) {
this.showPracticeImages(actionName);
} else if (actionName.startsWith('start_case_')) {
const caseNum = actionName.replace('start_case_', '');
this.startCase('case' + caseNum);
} else if (actionName === 'end_game') {
// Ends...
}
}
// ====== CASE INVESTIGATION SYSTEM ======
startCase(caseKey) {
const caseInfo = this.caseData[caseKey];
if (!caseInfo) { console.error('Case not found:', caseKey); return; }
this.currentCase = caseInfo;
this.caseStartTime = null;
this.caseHadError = false;
if (!this.caseGrades) this.caseGrades = {};
// Phase 1: Show case title card
this.showCaseTitleCard(caseInfo);
}
showCaseTitleCard(caseInfo) {
// Hide any leftover images from previous phases
this.elTarget.classList.add('hidden');
const wrapper = document.getElementById('suspect-wrapper');
if (wrapper) wrapper.innerHTML = '';
// Full screen overlay with case title
const overlay = document.createElement('div');
overlay.id = 'case-title-overlay';
overlay.className = 'fixed inset-0 z-[200] flex flex-col items-center justify-center bg-black/90 backdrop-blur-sm cursor-pointer';
overlay.innerHTML = `
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em] mb-4 animate-pulse">CASE FILE #00${caseInfo.id}</div>
<div class="text-5xl font-black text-white mb-4 tracking-wider" style="text-shadow: 0 0 30px rgba(217,70,239,0.6);">${caseInfo.title}</div>
<div class="font-tech text-2xl text-slate-400 tracking-[0.3em]">${caseInfo.titleEn}</div>
<div class="absolute bottom-12 text-fuchsia-400 text-sm animate-bounce font-tech">▼ CLICK TO START</div>
`;
overlay.onclick = () => {
overlay.remove();
this.showCaseDescription(caseInfo);
};
document.body.appendChild(overlay);
}
showCaseDescription(caseInfo) {
// Show case image + description in an overlay
const overlay = document.createElement('div');
overlay.id = 'case-desc-overlay';
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-sm cursor-pointer';
overlay.innerHTML = `
<div class="flex flex-col md:flex-row gap-8 max-w-5xl w-full px-8 items-center">
<img src="${caseInfo.caseImage}" class="h-[50vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Case Image">
<div class="flex flex-col gap-4 flex-1">
<div class="font-tech text-fuchsia-400 text-sm tracking-[0.3em]">CASE FILE #00${caseInfo.id}</div>
<h2 class="text-3xl font-black text-white">${caseInfo.title}</h2>
<p class="text-slate-300 text-lg leading-relaxed">${caseInfo.description}</p>
<div class="text-fuchsia-400 text-sm animate-bounce font-tech mt-4">▼ CLICK TO INVESTIGATE</div>
</div>
</div>
`;
overlay.onclick = () => {
overlay.remove();
this.showCaseSuspectsPhase(caseInfo);
};
document.body.appendChild(overlay);
}
showCaseSuspectsPhase(caseInfo) {
// Start timer NOW
this.caseStartTime = Date.now();
// Show evidence + suspects in the main scene area
this.showDialogue(false); // hide dialogue
this.elTarget.classList.remove('hidden');
document.getElementById('evidence-panel').classList.add('hidden');
const sprite = document.getElementById('speaker-sprite');
if (sprite) sprite.classList.add('hidden');
const wrapper = document.getElementById('suspect-wrapper');
wrapper.innerHTML = '';
wrapper.className = 'relative w-full flex flex-col items-center gap-6 py-4';
// Top row: Evidence image
const evidenceSection = document.createElement('div');
evidenceSection.className = 'flex flex-col items-center gap-2';
evidenceSection.innerHTML = `
<div class="font-tech text-fuchsia-400 text-sm tracking-widest">EVIDENCE / 蛛絲馬跡</div>
<img src="${caseInfo.evidenceImage}" class="h-[35vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Evidence">
`;
// Bottom row: Suspects (clickable)
const suspectSection = document.createElement('div');
suspectSection.className = 'flex flex-col items-center gap-2';
suspectSection.innerHTML = `<div class="font-tech text-cyan-400 text-sm tracking-widest">SUSPECTS / 嫌疑犯 <span class="text-xs text-slate-500">(點擊選擇犯人)</span></div>`;
const suspectRow = document.createElement('div');
suspectRow.className = 'flex gap-6 justify-center items-end';
caseInfo.suspects.forEach(s => {
const card = document.createElement('div');
card.className = 'flex flex-col items-center gap-1 cursor-pointer group transition-all duration-300';
card.innerHTML = `
<img src="${s.src}" class="h-[32vh] object-contain rounded-xl border-2 border-slate-600 group-hover:border-fuchsia-500 shadow-lg group-hover:shadow-fuchsia-500/30 transition-all duration-300 group-hover:scale-105" alt="${s.name}">
<span class="text-slate-400 group-hover:text-fuchsia-400 text-sm font-bold transition-colors">${s.name}</span>
`;
card.onclick = () => this.onSuspectClick(s, caseInfo);
suspectRow.appendChild(card);
});
suspectSection.appendChild(suspectRow);
wrapper.appendChild(evidenceSection);
wrapper.appendChild(suspectSection);
}
onSuspectClick(suspect, caseInfo) {
// Confirm dialog
const overlay = document.createElement('div');
overlay.id = 'confirm-overlay';
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm';
overlay.innerHTML = `
<div class="glass-panel p-8 rounded-xl flex flex-col gap-6 max-w-md w-full items-center">
<img src="${suspect.src}" class="h-[20vh] object-contain rounded-lg border border-fuchsia-500/40" alt="${suspect.name}">
<h3 class="text-2xl font-bold text-white">確認 <span class="text-fuchsia-400">${suspect.name}</span> 是犯人嗎?</h3>
<div class="flex gap-4 w-full">
<button id="confirm-yes" class="flex-1 bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-3 rounded-lg text-lg transition-all border border-fuchsia-400/50 hover:scale-105 transform duration-200">確認逮捕</button>
<button id="confirm-no" class="flex-1 bg-slate-700 hover:bg-slate-600 text-white font-bold py-3 rounded-lg text-lg transition-all border border-slate-500/50 hover:scale-105 transform duration-200">再想想</button>
</div>
</div>
`;
document.body.appendChild(overlay);
document.getElementById('confirm-no').onclick = () => overlay.remove();
document.getElementById('confirm-yes').onclick = () => {
overlay.remove();
this.onSuspectConfirmed(suspect, caseInfo);
};
}
onSuspectConfirmed(suspect, caseInfo) {
const isCorrectSuspect = (suspect.id === caseInfo.correctSuspect);
if (!isCorrectSuspect) {
this.caseHadError = true;
// Show wrong feedback but let them try again
const wrongOverlay = document.createElement('div');
wrongOverlay.id = 'wrong-suspect-overlay';
wrongOverlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer';
wrongOverlay.innerHTML = `
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 max-w-md w-full items-center border-2 border-red-500/50">
<div class="text-red-500 font-tech text-3xl font-bold">✗ WRONG SUSPECT</div>
<p class="text-slate-300 text-lg text-center">這不是犯人!請再仔細比對蛛絲馬跡和嫌疑犯的特徵。</p>
<div class="text-red-400 text-sm font-tech animate-bounce">▼ CLICK TO RETRY</div>
</div>
`;
wrongOverlay.onclick = () => wrongOverlay.remove();
document.body.appendChild(wrongOverlay);
return;
}
// Correct suspect! Now ask for proof method
this.askProofMethod(suspect, caseInfo);
}
askProofMethod(suspect, caseInfo) {
const suspectImg = suspect ? suspect.src : caseInfo.suspects.find(s => s.id === caseInfo.correctSuspect).src;
const suspectName = suspect ? suspect.name : caseInfo.suspects.find(s => s.id === caseInfo.correctSuspect).name;
const overlay = document.createElement('div');
overlay.id = 'proof-overlay';
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm';
overlay.innerHTML = `
<div class="glass-panel p-8 rounded-xl flex flex-col gap-6 max-w-4xl w-full items-center">
<div class="font-tech text-green-400 text-2xl font-bold">✓ SUSPECT IDENTIFIED</div>
<div class="flex gap-8 justify-center items-start w-full">
<div class="flex flex-col items-center gap-2">
<div class="font-tech text-fuchsia-400 text-xs tracking-widest">EVIDENCE / 蛛絲馬跡</div>
<img src="${caseInfo.evidenceImage}" class="h-[30vh] object-contain rounded-xl border-2 border-fuchsia-500/40 shadow-lg shadow-fuchsia-500/20" alt="Evidence">
</div>
<div class="flex flex-col items-center gap-2">
<div class="font-tech text-cyan-400 text-xs tracking-widest">SUSPECT / ${suspectName}</div>
<img src="${suspectImg}" class="h-[30vh] object-contain rounded-xl border-2 border-cyan-500/40 shadow-lg shadow-cyan-500/20" alt="Suspect">
</div>
</div>
<p class="text-slate-300 text-lg text-center">你要用哪種方法來 <span class="text-fuchsia-400 font-bold">證明</span> 這位嫌疑犯就是犯人?</p>
<div class="flex gap-4 w-full max-w-md">
<button class="proof-btn flex-1 bg-gradient-to-br from-pink-600 to-fuchsia-700 hover:from-pink-500 hover:to-fuchsia-600 text-white font-bold py-4 rounded-lg text-2xl font-tech transition-all border border-fuchsia-400/50 hover:scale-105 transform duration-200 shadow-lg shadow-fuchsia-500/20" data-method="SSS">SSS</button>
<button class="proof-btn flex-1 bg-gradient-to-br from-amber-600 to-yellow-700 hover:from-amber-500 hover:to-yellow-600 text-white font-bold py-4 rounded-lg text-2xl font-tech transition-all border border-yellow-400/50 hover:scale-105 transform duration-200 shadow-lg shadow-yellow-500/20" data-method="SAS">SAS</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.querySelectorAll('.proof-btn').forEach(btn => {
btn.onclick = () => {
const method = btn.dataset.method;
overlay.remove();
this.onProofMethodSelected(method, caseInfo);
};
});
}
onProofMethodSelected(method, caseInfo) {
const isCorrect = (method === caseInfo.correctMethod);
if (!isCorrect) {
this.caseHadError = true;
// Wrong method, let them pick again
const wrongOverlay = document.createElement('div');
wrongOverlay.id = 'wrong-method-overlay';
wrongOverlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/70 backdrop-blur-sm cursor-pointer';
wrongOverlay.innerHTML = `
<div class="glass-panel p-8 rounded-xl flex flex-col gap-4 max-w-md w-full items-center border-2 border-red-500/50">
<div class="text-red-500 font-tech text-3xl font-bold">✗ WRONG METHOD</div>
<p class="text-slate-300 text-lg text-center">這個證明方法不正確!請重新觀察證據中的邊和角。</p>
<div class="text-red-400 text-sm font-tech animate-bounce">▼ CLICK TO RETRY</div>
</div>
`;
wrongOverlay.onclick = () => {
wrongOverlay.remove();
this.askProofMethod(null, caseInfo);
};
document.body.appendChild(wrongOverlay);
return;
}
// All correct! Calculate score
const elapsed = Math.floor((Date.now() - this.caseStartTime) / 1000);
const grade = this.calculateGrade(elapsed, this.caseHadError);
this.showCaseResult(caseInfo, elapsed, grade);
}
calculateGrade(seconds, hadError) {
if (hadError) {
// Max B if had any error
if (seconds <= 30) return 'B';
return 'C';
}
// All correct, grading by speed
if (seconds <= 10) return 'S';
if (seconds <= 20) return 'A++';
if (seconds <= 30) return 'A+';
if (seconds <= 60) return 'A';
return 'B';
}
showCaseResult(caseInfo, seconds, grade) {
const gradeColors = {
'S': 'from-amber-400 to-yellow-600',
'A++': 'from-fuchsia-400 to-pink-600',
'A+': 'from-purple-400 to-violet-600',
'A': 'from-cyan-400 to-blue-600',
'B': 'from-green-400 to-emerald-600',
'C': 'from-slate-400 to-gray-600'
};
const gradeMessages = {
'S': '神探降臨!完美破案!',
'A++': '超凡的推理能力!',
'A+': '出色的辦案速度!',
'A': '幹得漂亮!',
'B': '案件已解決。',
'C': '勉強過關...'
};
const overlay = document.createElement('div');
overlay.id = 'result-overlay';
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/85 backdrop-blur-md cursor-pointer';
overlay.innerHTML = `
<div class="flex flex-col items-center gap-6">
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em]">CASE #00${caseInfo.id} CLOSED</div>
<div class="text-3xl font-bold text-white">${caseInfo.title}</div>
<div class="text-8xl font-black bg-gradient-to-br ${gradeColors[grade]} bg-clip-text text-transparent" style="text-shadow: 0 0 40px rgba(217,70,239,0.4); -webkit-text-stroke: 1px rgba(255,255,255,0.1);">
${grade}
</div>
<div class="text-xl text-slate-300">${gradeMessages[grade]}</div>
<div class="font-tech text-slate-500 text-sm">耗時 ${seconds} 秒</div>
<div class="text-fuchsia-400 text-sm animate-bounce font-tech mt-4">▼ CLICK TO CONTINUE</div>
</div>
`;
overlay.onclick = () => {
overlay.remove();
this.caseGrades[caseInfo.id] = grade;
this.onCaseComplete(caseInfo);
};
document.body.appendChild(overlay);
}
onCaseComplete(caseInfo) {
if (caseInfo.nextCase) {
// Chain to next case
this.startCase(caseInfo.nextCase);
} else {
// All cases done — show summary
this.showFinalSummary();
}
}
showFinalSummary() {
const gradeOrder = ['S', 'A++', 'A+', 'A', 'B', 'C'];
const gradeScores = { 'S': 100, 'A++': 90, 'A+': 80, 'A': 70, 'B': 50, 'C': 30 };
const gradeColors = {
'S': 'text-amber-400', 'A++': 'text-fuchsia-400', 'A+': 'text-purple-400',
'A': 'text-cyan-400', 'B': 'text-green-400', 'C': 'text-slate-400'
};
// Calculate overall grade from average score
const grades = this.caseGrades;
const totalScore = Object.values(grades).reduce((sum, g) => sum + (gradeScores[g] || 0), 0);
const avgScore = totalScore / Object.keys(grades).length;
let overallGrade = 'C';
if (avgScore >= 95) overallGrade = 'S';
else if (avgScore >= 85) overallGrade = 'A++';
else if (avgScore >= 75) overallGrade = 'A+';
else if (avgScore >= 65) overallGrade = 'A';
else if (avgScore >= 45) overallGrade = 'B';
// Save to localStorage (only if better than existing)
const storageKey = 'math_city_score_congruence';
const currentBest = localStorage.getItem(storageKey) || '';
const currentBestIdx = gradeOrder.indexOf(currentBest);
const newIdx = gradeOrder.indexOf(overallGrade);
if (currentBestIdx === -1 || newIdx < currentBestIdx) {
localStorage.setItem(storageKey, overallGrade);
localStorage.setItem('math_city_score_congruence_val', avgScore.toString());
}
const caseTitles = { 1: '霓虹暗巷重傷害案', 2: '頂級俱樂部入侵案', 3: '市政廳塗鴉案' };
// Build case grade cards
let caseCardsHtml = '';
for (let i = 1; i <= 3; i++) {
const g = grades[i] || 'C';
caseCardsHtml += `
<div class="flex items-center gap-4 bg-slate-800/60 rounded-xl px-6 py-3 border border-slate-700">
<span class="font-tech text-fuchsia-500 text-sm">CASE ${i}</span>
<span class="text-white text-sm flex-1">${caseTitles[i]}</span>
<span class="text-2xl font-black ${gradeColors[g]}">${g}</span>
</div>
`;
}
const overallGradeColors = {
'S': 'from-amber-400 to-yellow-600', 'A++': 'from-fuchsia-400 to-pink-600',
'A+': 'from-purple-400 to-violet-600', 'A': 'from-cyan-400 to-blue-600',
'B': 'from-green-400 to-emerald-600', 'C': 'from-slate-400 to-gray-600'
};
const hl = (t) => `<span class="text-yellow-400 font-bold">${t}</span>`;
const overlay = document.createElement('div');
overlay.id = 'final-summary-overlay';
overlay.className = 'fixed inset-0 z-[200] flex items-center justify-center bg-black/90 backdrop-blur-md overflow-y-auto py-8';
overlay.innerHTML = `
<div class="flex flex-col items-center gap-6 max-w-3xl w-full px-6">
<div class="font-tech text-fuchsia-500 text-lg tracking-[0.5em]">MISSION COMPLETE</div>
<div class="text-4xl font-black text-white">辦案總結</div>
<!-- Overall Grade -->
<div class="text-8xl font-black bg-gradient-to-br ${overallGradeColors[overallGrade]} bg-clip-text text-transparent" style="-webkit-text-stroke: 1px rgba(255,255,255,0.1);">
${overallGrade}
</div>
<!-- Case Breakdown -->
<div class="flex flex-col gap-3 w-full max-w-lg">
${caseCardsHtml}
</div>
<!-- Summary Text -->
<div class="glass-panel rounded-xl p-6 max-w-lg w-full text-slate-300 leading-relaxed text-base space-y-4 border border-fuchsia-500/20">
<p>恭喜你成功協助警方破案了,未來或許你真的有機會成為一個好警探呢!</p>
<p>三角形全等是你學習數學以來,第一次碰到要${hl('「證明」')}的內容!證明的邏輯對於判斷事情的${hl('正確與否')}非常重要,或許也能提升你的${hl('吵架能力')}(?</p>
<p>一個三角形中,有${hl('3個邊')}${hl('3個角')},但每次都要看${hl('6個')}條件才能證明全等,實在太麻煩了,於是數學家想盡辦法將6個${hl('簡化')}${hl('3個')}條件即可判斷,除了剛剛學會的${hl('SSS')}${hl('SAS')}外,${hl('還有其他幾種')}喔!剩下的細節等上課的時候我們再慢慢說吧~</p>
</div>
<!-- Back Button -->
<a href="index.html" class="bg-fuchsia-600 hover:bg-fuchsia-500 text-white font-bold py-4 px-12 rounded-xl text-xl font-tech tracking-widest border border-fuchsia-400/50 transition-all hover:scale-105 transform duration-200 shadow-lg shadow-fuchsia-500/30 inline-flex items-center gap-3" style="text-decoration:none">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" 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>
回到 Math City
</a>
</div>
`;
document.body.appendChild(overlay);
}
showSSSSASImages() {
this.elTarget.classList.remove('hidden');
document.getElementById('evidence-panel').classList.add('hidden'); // Hide evidence panel
const wrapper = document.getElementById('suspect-wrapper');
// clear wrapper
wrapper.innerHTML = '';
// Add images side by side
const container = document.createElement('div');
container.className = 'flex gap-8 justify-center items-center';
const img1 = document.createElement('img');
img1.src = 'Assets/triangle/犯罪證明/紅.png';
img1.className = 'h-[40vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700';
const img2 = document.createElement('img');
img2.src = 'Assets/triangle/犯罪證明/SAS黃.png';
img2.className = 'h-[40vh] object-contain shadow-lg shadow-yellow-500/20 rounded-xl border border-slate-700';
container.appendChild(img1);
container.appendChild(img2);
wrapper.appendChild(container);
this.actionCompleted = true; // No interaction needed, just show
this.next(); // Since it's an action that completes immediately visually
}
showPracticeImages(actionName) {
this.elTarget.classList.remove('hidden');
document.getElementById('evidence-panel').classList.add('hidden');
const wrapper = document.getElementById('suspect-wrapper');
wrapper.innerHTML = '';
const container = document.createElement('div');
container.className = 'flex gap-8 justify-center items-center';
let src1, src2;
if (actionName === 'show_practice_1') {
src1 = 'Assets/triangle/蛛絲馬跡/4.png';
src2 = 'Assets/triangle/嫌疑犯/2農夫.png';
} else if (actionName === 'show_practice_2') {
src1 = 'Assets/triangle/蛛絲馬跡/1.png';
src2 = 'Assets/triangle/嫌疑犯/5冰淇淋師傅.png';
}
const img1 = document.createElement('img');
img1.src = src1;
img1.className = 'h-[70vh] object-contain shadow-lg shadow-fuchsia-500/20 rounded-xl border border-slate-700'; // Increased size
const img2 = document.createElement('img');
img2.src = src2;
img2.className = 'h-[70vh] object-contain shadow-lg shadow-cyan-500/20 rounded-xl border border-slate-700'; // Increased size
container.appendChild(img1);
container.appendChild(img2);
wrapper.appendChild(container);
this.actionCompleted = true;
this.next();
}
startInvestigation() {
this.elTarget.classList.remove('hidden');
document.getElementById('evidence-panel').classList.remove('hidden');
// Switch to the Feature Logic Image
const img = document.getElementById('suspect-image');
img.src = 'Assets/triangle/嫌疑犯/16造型師.png'; // Use new image with marks
this.foundFeatures.clear();
this.angleCount = 0;
this.sideCount = 0;
this.updateStatsDisplay();
this.updateEvidenceCount();
// Adjusted positions to match "Red/Blue Circles"
const hitboxes = [
// Angles (Red)
// Top Vertex (Right-ish) - Averaged
{ id: 'angle_top', x: 70, y: 26, type: 'Angle', class: 'found-angle' },
// Bottom Left Vertex - Averaged
{ id: 'angle_left', x: 20, y: 53, type: 'Angle', class: 'found-angle' },
// Bottom Right Vertex - Averaged
{ id: 'angle_right', x: 70, y: 53, type: 'Angle', class: 'found-angle' },
// Sides (Blue)
// Hypotenuse (Midpoint of Top and Left) - Averaged
{ id: 'side_hypotenuse', x: 45, y: 38, type: 'Side', class: 'found-side' },
// Right Side (Midpoint of Top and Right) - Averaged
{ id: 'side_right', x: 70, y: 40, type: 'Side', class: 'found-side' },
// Bottom Side (Midpoint of Left and Right) - Averaged
{ id: 'side_bottom', x: 45, y: 53, type: 'Side', class: 'found-side' }
];
// Target the wrapper logic
const wrapper = document.getElementById('suspect-wrapper');
const existing = wrapper.querySelectorAll('.hitbox');
existing.forEach(e => e.remove());
hitboxes.forEach(hb => {
const el = document.createElement('div');
el.className = 'hitbox';
el.style.left = hb.x + '%';
el.style.top = hb.y + '%';
el.style.transform = 'translate(-50%, -50%)';
el.dataset.id = hb.id;
el.onclick = (e) => this.onFeatureClick(e, el, hb);
wrapper.appendChild(el);
});
}
onFeatureClick(e, el, hb) {
e.stopPropagation();
if (this.foundFeatures.has(el.dataset.id)) return;
el.classList.add(hb.class); // Use specific color class
this.foundFeatures.add(el.dataset.id);
if (hb.type === 'Angle') this.angleCount++;
if (hb.type === 'Side') this.sideCount++;
this.updateStatsDisplay();
this.updateEvidenceCount(); // Keeps the top-right counter too
// this.showFloatingText(e.clientX, e.clientY, hb.type + ' Found!');
if (hb.type === 'Angle') el.innerText = 'A';
if (hb.type === 'Side') el.innerText = 'S';
if (this.foundFeatures.size >= this.requiredFeatures) {
this.completeInvestigation();
}
}
updateStatsDisplay() {
const display = document.getElementById('stats-display');
if (display) {
display.innerText = `[已找到邊數:${this.sideCount},已找到角數:${this.angleCount}]`;
}
}
updateEvidenceCount() {
const count = document.getElementById('evidence-count');
count.innerText = `${this.foundFeatures.size} / ${this.requiredFeatures}`;
}
showFloatingText(x, y, text) {
const el = document.createElement('div');
el.className = 'fixed pointer-events-none text-white font-bold text-shadow animate-float-up z-50 text-xl';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.innerText = text;
document.body.appendChild(el);
setTimeout(() => el.remove(), 1000);
}
completeInvestigation() {
this.actionCompleted = true;
setTimeout(() => {
this.step++;
// Add next dialogue/logic
const fmtName = (n) => `<span class="text-amber-400 font-bold mx-1 text-xl">${n}</span>`;
const nextPart = [
{
type: 'dialogue',
speaker: '資深警探',
type: 'dialogue',
speaker: '資深警探',
text: '很好,看來我們真的找對人了,三角形的所有<span class="text-yellow-400 font-bold">特徵</span>就是他的<span class="text-yellow-400 font-bold">三個邊</span>和<span class="text-yellow-400 font-bold">三個角</span>,如果六項特徵全部都符合的話,那兩個三角形一定能<span class="text-yellow-400 font-bold">完全重疊</span>在一起,表示這是<span class="text-yellow-400 font-bold">同一個三角形</span>。'
},
{
type: 'dialogue',
speaker: '資深警探',
text: '但是我們在犯罪現場的證據,並沒有辦法這麼完整,現在我要教你<span class="text-yellow-400 font-bold">2種更好用的</span>辨認方式!'
},
{
type: 'dialogue',
speaker: '資深警探',
text: '首先要教你一些我們的專用術語 <span class="text-yellow-400">S=Side=三角形的邊</span>,<span class="text-yellow-400">A=Angle=三角形的角</span>'
},
{
type: 'quiz',
question: '現在請你告訴我,將「SSS」換成中文是什麼?',
answer: ['邊邊邊', '三邊由', '三邊', '三個邊'],
correctMsg: '沒錯!S就是邊,SSS就是邊邊邊!',
wrongMsg: '不對喔... (提示:S是邊,SSS就是?)'
},
{
type: 'quiz',
question: '那麼,「SAS」換成中文是什麼?',
answer: ['邊角邊'],
correctMsg: '太好了,你學得很快嘛!',
wrongMsg: '再想想... (提示:S是邊,A是角,SAS就是?)'
},
{
type: 'dialogue',
speaker: '資深警探',
text: '接下來我要教你的高級刑偵技術,也就是證明犯人的方法<span class="text-yellow-400 font-bold">SSS</span>和<span class="text-yellow-400 font-bold">SAS</span>'
},
{
type: 'action',
action: 'show_sss_sas_images',
speaker: '資深警探',
text: '<span class="text-yellow-400 font-bold">SSS</span>:線索中的三個邊長和嫌疑犯的三個邊長完全相等<br><span class="text-yellow-400 font-bold">SAS</span>:線索中的兩個邊和夾起來的角,與嫌疑犯的兩個邊和夾起來的角相等<br><br>只要有找到其中一種,就能幫我們證明他就是犯人!這樣你學會了嗎?'
},
{
type: 'quiz',
question: '請問你剛剛學到的證明方法其中一個是?(英文)',
answer: ['SSS', 'SAS'],
correctMsg: '沒錯!這是其中一個!',
wrongMsg: '請輸入SSS或SAS'
},
{
type: 'quiz',
question: '另一個是?',
answer: ['SSS', 'SAS'], // Logic: User should enter the other one, but simplified game engine checks inclusion.
// To properly 'exclude' the previous answer without complex logic injection, we'll just accept both for now
// but the user phrasing implies they should know the other.
// Given constraints, I'll accept both to avoid getting stuck if they repeat.
correctMsg: '太棒了!你都記住了!',
wrongMsg: '請輸入另一個證明方法 (SSS或SAS)'
},
{
type: 'quiz',
question: '那SSS的中文是?',
answer: ['邊邊邊'],
correctMsg: '完全正確!',
wrongMsg: '提示:S是邊...'
},
{
type: 'quiz',
question: '還有SAS的中文是?',
answer: ['邊角邊'],
correctMsg: '太強了!你已經具備資深警探的潛力了!',
wrongMsg: '提示:A是角...'
},
{
type: 'dialogue',
speaker: '資深警探',
text: '接下來要測驗你剛剛學到的證明方法囉!'
},
{
type: 'action',
action: 'show_practice_1',
speaker: '資深警探',
text: '請看這兩張圖...'
},
{
type: 'quiz',
question: '請問要用哪個證明方法說明兩者是同一人呢?(SSS或SAS)',
answer: ['SSS'],
correctMsg: '答對了!三邊對應相等!',
wrongMsg: '再仔細看看,是三個邊相等還是兩邊一角?'
},
{
type: 'action',
action: 'show_practice_2',
speaker: '資深警探',
text: '再來試試這一題...'
},
{
type: 'quiz',
question: '請問這題要用哪個證明方法說明兩者是同一人呢?(SSS或SAS)',
answer: ['SAS'],
correctMsg: '太厲害了!兩邊一夾角對應相等!',
wrongMsg: '再仔細看看,有幾個邊?有幾個角?'
},
{
type: 'dialogue',
speaker: '資深警探',
text: (name) => `太好了,既然${fmtName(name)}成功通過測驗了,接下來有幾個案件要請你幫忙了!`
},
{
type: 'dialogue',
speaker: '資深警探',
text: '我們會為你的辦案狀況打分數,請以<span class="text-yellow-400 font-bold">正確性</span>為<span class="text-yellow-400 font-bold">優先</span>,速度為次,抓錯犯人可是很傷腦筋的呢~'
},
{
type: 'action',
action: 'start_case_1',
speaker: 'SYSTEM',
text: '案件載入中...'
}
];
// Replace remaining script or append?
// Since SCRIPT is simpler now, we can just splice.
// NOTE: Previous logic used `startInvestigation` at step X.
// We need to inject these steps AFTER the current step.
// Remove old placeholder steps if any.
SCRIPT.splice(this.step, SCRIPT.length - this.step, ...nextPart);
// Reset step index to process the first inserted item
this.processStep();
}, 1000);
}
appendNextPhase() {
// Deprecated in favor of direct injection in completeInvestigation
}
showQuiz(data) {
const quizLayer = document.getElementById('quiz-layer');
const quizContent = document.getElementById('quiz-content');
quizLayer.classList.remove('hidden');
// Integrated layout matching dialogue style exactly
quizContent.innerHTML = `
<div class="dialogue-name">資深警探 <span class="text-xs text-slate-500 ml-2 tracking-normal">// AUTHENTICATION REQUIRED</span></div>
<div class="dialogue-text mb-4">${data.question}</div>
<div class="flex gap-4 w-full mt-auto items-end">
<div class="text-fuchsia-500 font-tech text-xl animate-pulse">&gt;</div>
<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">
<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>
</div>
<div id="quiz-feedback" class="absolute top-6 right-8 font-tech text-xl font-bold"></div>
`;
const input = document.getElementById('quiz-input');
const btn = document.getElementById('quiz-submit');
const feedback = document.getElementById('quiz-feedback');
const checkAnswer = () => {
const val = input.value.trim();
if (data.answer.includes(val)) {
// Correct
feedback.className = 'text-green-400 font-bold';
feedback.innerText = 'ACCESS GRANTED';
input.classList.add('text-green-400');
input.disabled = true;
keypad.close(); // 關閉小鍵盤
setTimeout(() => {
quizLayer.classList.add('hidden');
// Add temporary dialogue for success reaction
this.showDialogue(true);
this.setSpeaker('資深警探');
this.typeText(data.correctMsg);
}, 1000);
} else {
// Wrong
input.value = '';
input.classList.add('animate-shake');
// Clear shake separately
setTimeout(() => {
input.classList.remove('animate-shake');
}, 500);
// Show persistent wrong message
feedback.className = 'text-red-400 text-lg font-bold';
feedback.innerText = data.wrongMsg;
}
};
btn.onclick = checkAnswer;
input.onkeypress = (e) => { if (e.key === 'Enter') checkAnswer(); };
// 自動開啟小鍵盤(觸控裝置)或點擊輸入框時開啟
if (isTouchDevice) {
// 觸控裝置自動開啟
setTimeout(() => {
keypad.open(input, checkAnswer);
}, 300);
}
// 點擊輸入框也可以開啟小鍵盤
input.addEventListener('click', (e) => {
e.preventDefault();
keypad.open(input, checkAnswer);
});
input.addEventListener('focus', (e) => {
if (isTouchDevice) {
e.preventDefault();
keypad.open(input, checkAnswer);
}
});
}
// Add shake animation style dynamically
addStyles() {
const style = document.createElement('style');
style.innerHTML = `
@keyframes float-up {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(-50px); opacity: 0; }
}
.animate-float-up {
animation: float-up 1s ease-out forwards;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.animate-shake {
animation: shake 0.3s ease-in-out;
}
`;
document.head.appendChild(style);
}
}
// Init Game
window.onload = () => {
window.game = new GameEngine();
};
</script>
</body>
</html>