This commit is contained in:
lunaticbum 2025-09-01 17:57:59 +09:00
parent 90f0ef1cce
commit 543b1209e6
3 changed files with 166 additions and 34 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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>