...
This commit is contained in:
parent
90f0ef1cce
commit
543b1209e6
@ -51,6 +51,32 @@ body {
|
||||
max-width: 500px; /* 보드와 비슷한 너비로 설정 */
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.2em;
|
||||
flex-wrap: wrap; /* Allow controls to wrap on small screens */
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* (★ ADD) Styles for the mode selector */
|
||||
#mode-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
background-color: #eee;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#mode-selector label {
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
/* Style for the selected radio button's label */
|
||||
#mode-selector input:checked + span {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
/* Hide the actual radio buttons */
|
||||
#mode-selector input {
|
||||
display: none;
|
||||
}
|
||||
#hint-btn {
|
||||
padding: 8px 15px;
|
||||
|
||||
@ -17,6 +17,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// --- DOM 요소 참조 ---
|
||||
const modeSelector = document.getElementById('mode-selector');
|
||||
const gameBoard = document.getElementById('game-board');
|
||||
const pointsDisplay = document.getElementById('points-display');
|
||||
const hintBtn = document.getElementById('hint-btn');
|
||||
@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const modalButtons = document.getElementById('modal-buttons');
|
||||
|
||||
// --- 게임 상태를 관리하는 변수 ---
|
||||
let currentMode = 'fill';
|
||||
let points = 5;
|
||||
let isGameFinished = false;
|
||||
let isDragging = false;
|
||||
@ -44,6 +46,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let lockedRows = Array(numRows).fill(false);
|
||||
let lockedCols = Array(numCols).fill(false);
|
||||
|
||||
/**
|
||||
* Updates the currentMode based on the selected radio button
|
||||
*/
|
||||
function updateMode() {
|
||||
currentMode = document.querySelector('input[name="play-mode"]:checked').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 폰트 높이와 두 자릿수 숫자 너비를 기준으로 최적의 셀 크기를 계산합니다.
|
||||
* @returns {number} 계산된 각 그리드 셀의 크기 (px)
|
||||
@ -173,47 +182,88 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 상호작용 그리드에 마우스 이벤트 리스너를 등록합니다.
|
||||
* (★ REVISED) Event Listeners for Mouse and Touch
|
||||
*/
|
||||
function attachEventListeners(grid) {
|
||||
grid.addEventListener('mousedown', (e) => {
|
||||
if (isGameFinished || !e.target.classList.contains('grid-cell')) return;
|
||||
isDragging = true;
|
||||
e.preventDefault();
|
||||
const cell = e.target;
|
||||
const row = parseInt(cell.dataset.row);
|
||||
const col = parseInt(cell.dataset.col);
|
||||
startCell = { row, col };
|
||||
lastHoveredCell = startCell;
|
||||
const currentState = playerGrid[row][col];
|
||||
if (e.button === 0) {
|
||||
dragAction = (currentState === 1) ? 'clear' : 'fill';
|
||||
} else if (e.button === 2) {
|
||||
dragAction = (currentState === -1) ? 'clear' : 'mark';
|
||||
}
|
||||
updateSelectionVisuals();
|
||||
});
|
||||
grid.addEventListener('mouseover', (e) => {
|
||||
if (!isDragging || !e.target.classList.contains('grid-cell')) return;
|
||||
const row = parseInt(e.target.dataset.row);
|
||||
const col = parseInt(e.target.dataset.col);
|
||||
// --- MOUSE EVENTS ---
|
||||
grid.addEventListener('mousedown', (e) => handleDragStart(e));
|
||||
grid.addEventListener('mouseover', (e) => handleDragMove(e));
|
||||
grid.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
|
||||
// --- TOUCH EVENTS ---
|
||||
grid.addEventListener('touchstart', (e) => handleDragStart(e), { passive: false });
|
||||
grid.addEventListener('touchmove', (e) => handleDragMove(e), { passive: false });
|
||||
}
|
||||
|
||||
// End drag for both mouse and touch
|
||||
window.addEventListener('mouseup', () => handleDragEnd());
|
||||
window.addEventListener('touchend', () => handleDragEnd());
|
||||
|
||||
// Listen for changes on the mode selector
|
||||
modeSelector.addEventListener('change', updateMode);
|
||||
|
||||
/**
|
||||
* (★ NEW) Handles the start of a drag (mousedown or touchstart)
|
||||
*/
|
||||
function handleDragStart(e) {
|
||||
if (isGameFinished || !e.target.classList.contains('grid-cell')) return;
|
||||
isDragging = true;
|
||||
e.preventDefault();
|
||||
|
||||
const cell = e.target;
|
||||
const currentState = playerGrid[parseInt(cell.dataset.row)][parseInt(cell.dataset.col)];
|
||||
|
||||
// Determine action based on mode, not button
|
||||
if (currentMode === 'fill') {
|
||||
dragAction = (currentState === 1) ? 'clear' : 'fill';
|
||||
} else { // currentMode === 'mark'
|
||||
dragAction = (currentState === -1) ? 'clear' : 'mark';
|
||||
}
|
||||
|
||||
// Update selection visuals
|
||||
startCell = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
|
||||
lastHoveredCell = startCell;
|
||||
updateSelectionVisuals();
|
||||
}
|
||||
|
||||
/**
|
||||
* (★ NEW) Handles movement during a drag (mouseover or touchmove)
|
||||
*/
|
||||
function handleDragMove(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
|
||||
// For touch events, we need to find the element under the finger
|
||||
const target = (e.touches)
|
||||
? document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY)
|
||||
: e.target;
|
||||
|
||||
if (target && target.classList.contains('grid-cell')) {
|
||||
const row = parseInt(target.dataset.row);
|
||||
const col = parseInt(target.dataset.col);
|
||||
|
||||
if (row !== lastHoveredCell.row || col !== lastHoveredCell.col) {
|
||||
lastHoveredCell = { row, col };
|
||||
updateSelectionVisuals();
|
||||
}
|
||||
});
|
||||
grid.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
}
|
||||
|
||||
// 드래그가 화면 어디에서 끝나든 감지하기 위해 window에 등록
|
||||
window.addEventListener('mouseup', () => {
|
||||
/**
|
||||
* (★ NEW) Handles the end of a drag (mouseup or touchend)
|
||||
*/
|
||||
function handleDragEnd() {
|
||||
if (!isDragging) return;
|
||||
|
||||
currentSelection.forEach(cell => updateCellState(cell, dragAction));
|
||||
clearSelectionVisuals();
|
||||
|
||||
if (dragAction === 'fill' || dragAction === 'clear') {
|
||||
checkAndLockCompletedLines(affectedRows, affectedCols);
|
||||
}
|
||||
checkWinCondition();
|
||||
|
||||
// Reset state
|
||||
isDragging = false;
|
||||
dragAction = null;
|
||||
startCell = null;
|
||||
@ -221,7 +271,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentSelection.clear();
|
||||
affectedRows.clear();
|
||||
affectedCols.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 드래그 중인 사각형 영역을 계산하고 시각적으로 업데이트하는 함수
|
||||
@ -361,13 +411,59 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
isGameFinished = true;
|
||||
document.querySelector('.puzzle-grid-container').style.pointerEvents = 'none';
|
||||
hintBtn.disabled = true;
|
||||
// ... (성공 애니메이션 로직은 여기에 추가될 수 있습니다)
|
||||
showResultModal({
|
||||
title: 'Success! 🎉', message: '퍼즐을 완성했습니다!', buttons: [
|
||||
{ text: '다른 문제 풀기', class: 'primary', action: () => window.location.href = '/puzzle/play' },
|
||||
{ text: '홈으로 (Home)', action: () => window.location.href = '/' }
|
||||
]
|
||||
});
|
||||
// --- 요소 참조 ---
|
||||
const puzzleGridContainer = document.querySelector('.puzzle-grid-container');
|
||||
const grayscaleImg = document.getElementById('grayscale-reveal');
|
||||
const originalImg = document.getElementById('original-reveal');
|
||||
|
||||
// --- 상호작용 비활성화 ---
|
||||
puzzleGridContainer.style.pointerEvents = 'none';
|
||||
hintBtn.disabled = true;
|
||||
|
||||
// --- 애니메이션 준비 ---
|
||||
// 1. 이미지 소스 설정
|
||||
// (puzzleData에 저장해 둔 Base64 인코딩된 이미지 데이터를 사용합니다)
|
||||
grayscaleImg.src = puzzleData.grayscaleImage;
|
||||
originalImg.src = puzzleData.originalImage;
|
||||
|
||||
// 2. 현재 보드의 transform(스케일) 값을 이미지에도 동일하게 적용하여 정렬
|
||||
// 이렇게 해야 반응형으로 크기가 조절된 보드 위에도 이미지가 정확히 겹칩니다.
|
||||
const boardTransform = gameBoard.style.transform;
|
||||
grayscaleImg.style.transform = boardTransform;
|
||||
originalImg.style.transform = boardTransform;
|
||||
|
||||
// --- 애니메이션 순차 실행 ---
|
||||
setTimeout(() => {
|
||||
// 3. 그레이스케일 이미지 페이드 인
|
||||
// (CSS transition 속성에 의해 부드럽게 나타납니다)
|
||||
grayscaleImg.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
// 4. 컬러 이미지 페이드 인 (크로스페이드 효과)
|
||||
// (그레이스케일 이미지 위에 컬러 이미지가 겹쳐지며 나타납니다)
|
||||
originalImg.style.opacity = '1';
|
||||
|
||||
setTimeout(() => {
|
||||
// 5. 애니메이션이 모두 끝난 후 최종 결과 모달 표시
|
||||
showResultModal({
|
||||
title: 'Success! 🎉',
|
||||
message: '퍼즐을 완성했습니다!',
|
||||
buttons: [
|
||||
{ text: '다른 문제 풀기', class: 'primary', action: () => window.location.href = '/puzzle/play' },
|
||||
{ text: '홈으로 (Home)', action: () => window.location.href = '/' }
|
||||
]
|
||||
});
|
||||
}, 2000); // 컬러 이미지가 나타나고 2초 후
|
||||
|
||||
}, 2000); // 그레이스케일 이미지가 나타나고 2초 후
|
||||
|
||||
}, 500); // 마지막 셀을 채우고 0.5초 후 시작
|
||||
// showResultModal({
|
||||
// title: 'Success! 🎉', message: '퍼즐을 완성했습니다!', buttons: [
|
||||
// { text: '다른 문제 풀기', class: 'primary', action: () => window.location.href = '/puzzle/play' },
|
||||
// { text: '홈으로 (Home)', action: () => window.location.href = '/' }
|
||||
// ]
|
||||
// });
|
||||
}
|
||||
|
||||
// 힌트 버튼 클릭 이벤트 처리
|
||||
@ -409,6 +505,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const optimalCellSize = calculateCellSize();
|
||||
drawBoard(optimalCellSize);
|
||||
updatePointsDisplay();
|
||||
updateMode(); // Set initial mode
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fitBoardToScreen();
|
||||
|
||||
@ -19,6 +19,15 @@
|
||||
<h1>Solve the Puzzle! 🧩</h1>
|
||||
|
||||
<div id="game-controls">
|
||||
<div id="mode-selector">
|
||||
<label>
|
||||
<input type="radio" name="play-mode" value="fill" checked> Fill
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="play-mode" value="mark"> Mark (X)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="points-info">
|
||||
❤️ Points: <span id="points-display">5</span>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user