...
This commit is contained in:
@@ -6,878 +6,31 @@
|
||||
layout:decorate="~{layout/default_layout}"
|
||||
>
|
||||
<th:block layout:fragment="head" id="head">
|
||||
<link th:href="@{/css/common_game_theme.css}" rel="stylesheet" />
|
||||
<style>
|
||||
/* === nonogram.css (게임 플레이용) === */
|
||||
#board-viewport {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 95vw; /* 화면 너비에 좀 더 맞춤 */
|
||||
margin: 20px auto;
|
||||
/* (★ 핵심 수정) Flexbox로 자식 요소를 중앙 정렬합니다. */
|
||||
display: flex;
|
||||
justify-content: center; /* 자식 요소를 수평 중앙 정렬 */
|
||||
align-items: flex-start; /* 위쪽에 정렬 */
|
||||
}
|
||||
.reveal-img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0; /* Hidden by default */
|
||||
pointer-events: none; /* Make them unclickable */
|
||||
transition: opacity 1.5s ease-in-out; /* Fade animation */
|
||||
transform-origin: top left; /* Align with the game board's scaling */
|
||||
}
|
||||
|
||||
.guide-line-right {
|
||||
border-right: 2px solid #999 !important;
|
||||
}
|
||||
|
||||
.guide-line-bottom {
|
||||
border-bottom: 2px solid #999 !important;
|
||||
}
|
||||
|
||||
#game-board {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
background-color: #999;
|
||||
border: 2px solid #333;
|
||||
transform-origin: top;
|
||||
}
|
||||
#game-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
#mode-selector {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#mode-selector label {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#mode-selector span {
|
||||
padding: 8px 15px;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
#mode-selector input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mode-selector input[type="radio"]:checked + span {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
#hint-btn {
|
||||
padding: 8px 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#hint-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.col-clues-container, .row-clues-container {
|
||||
display: flex;
|
||||
}
|
||||
.row-clues-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.puzzle-grid-container {
|
||||
display: grid;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
/* nonogram.css의 .clue-cell (게임용) */
|
||||
.clue-cell {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.row-clue {
|
||||
justify-content: flex-end; /* 힌트 오른쪽 정렬 */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.col-clue {
|
||||
justify-content: center; /* 힌트 가운데 정렬 */
|
||||
align-items: flex-end; /* 힌트 아래쪽 정렬 */
|
||||
text-align: center;
|
||||
line-height: 1.2; /* 줄 간격 */
|
||||
}
|
||||
|
||||
/* nonogram.css의 .grid-cell (게임용) */
|
||||
.grid-cell {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* nonogram.css의 .filled (게임용) */
|
||||
.grid-cell.filled {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.grid-cell.marked::after {
|
||||
content: 'X';
|
||||
color: #ff5c5c;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-cell.incorrect {
|
||||
background-color: #ffcccc;
|
||||
animation: shake 0.5s;
|
||||
}
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
#result-modal {
|
||||
background-color: white;
|
||||
padding: 20px 40px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
#modal-title {
|
||||
margin-top: 0;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
#modal-buttons button {
|
||||
padding: 10px 20px;
|
||||
margin: 0 10px;
|
||||
font-size: 1em;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ccc;
|
||||
min-width: 120px;
|
||||
}
|
||||
#modal-buttons button.primary {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.clue-cell.completed {
|
||||
color: #999; /* 색상을 회색으로 */
|
||||
text-decoration: line-through; /* 취소선 */
|
||||
}
|
||||
|
||||
.grid-cell.locked {
|
||||
opacity: 0.8; /* 약간 투명하게 */
|
||||
}
|
||||
|
||||
.grid-cell.selecting {
|
||||
background-color: rgba(0, 123, 255, 0.3); /* 반투명 파란색 배경 */
|
||||
border-color: rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
|
||||
/* === puzzle.css (업로드 미리보기용) - 충돌 해결됨 === */
|
||||
|
||||
#puzzle-container {
|
||||
display: grid;
|
||||
/* We will set grid-template-columns/rows with JS */
|
||||
grid-gap: 2px;
|
||||
margin-top: 20px;
|
||||
background-color: #333;
|
||||
border: 2px solid #333;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* (★충돌 해결) #puzzle-container 내부의 .grid-cell (미리보기 코너 셀) */
|
||||
#puzzle-container .grid-cell {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
font-size: 14px;
|
||||
/* nonogram.css의 .grid-cell 스타일과 겹치지 않음 */
|
||||
cursor: default;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* (★충돌 해결) #puzzle-container 내부의 .clue-cell (미리보기 힌트 셀) */
|
||||
#puzzle-container .clue-cell {
|
||||
background-color: #cce7ff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
min-height: 25px;
|
||||
font-weight: bold;
|
||||
/* nonogram.css의 .clue-cell 스타일과 겹치지 않음 */
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.solution-cell {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
/* (★충돌 해결) .filled 대신 .solution-cell.filled 사용 */
|
||||
.solution-cell.filled {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
/* .empty는 .solution-cell.empty로 사용 (upload.js 기준) */
|
||||
.solution-cell.empty {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
#puzzle-wrapper {
|
||||
position: relative; /* Needed for absolute positioning of children */
|
||||
}
|
||||
|
||||
/* 이 ID는 nonogram.html에서 사용되지 않으므로 충돌 없음 */
|
||||
#success-animation-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none; /* Allows clicking through the container */
|
||||
}
|
||||
|
||||
#success-animation-container img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0; /* Hidden by default */
|
||||
transition: opacity 1.0s ease-in-out; /* Fade animation */
|
||||
}
|
||||
</style>
|
||||
</th:block >
|
||||
|
||||
<th:block layout:fragment="content">
|
||||
<div id="game-controls">
|
||||
<div id="mode-selector">
|
||||
<label>
|
||||
<input type="radio" name="play-mode" value="fill" checked><span> Fill</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="play-mode" value="mark"><span> Mark (X)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="points-info">
|
||||
❤️ Points: <span id="points-display">5</span>
|
||||
</div>
|
||||
<button id="hint-btn">Hint (-1 Point)</button>
|
||||
</div>
|
||||
|
||||
<div id="board-viewport">
|
||||
<div id="game-board">
|
||||
</div>
|
||||
<img id="grayscale-reveal" class="reveal-img" src="" alt="Grayscale version">
|
||||
<img id="original-reveal" class="reveal-img" src="" alt="Original version">
|
||||
|
||||
<div id="result-overlay" class="hidden">
|
||||
<div id="result-modal">
|
||||
<h2 id="modal-title"></h2>
|
||||
<p id="modal-message"></p>
|
||||
<div id="modal-buttons">
|
||||
<div class="game-body-wrapper">
|
||||
<h1>Nonogram Logic</h1>
|
||||
<div class="game-play-box wide">
|
||||
<div id="game-controls" style="margin: 0 0 20px 0; width:100%; display:flex; justify-content:space-between;">
|
||||
<div id="mode-selector">
|
||||
<label><input type="radio" name="play-mode" value="fill" checked><span>Fill</span></label>
|
||||
<label><input type="radio" name="play-mode" value="mark"><span>Mark</span></label>
|
||||
</div>
|
||||
<div id="points-info" class="score-board">❤️ <span id="points-display">5</span></div>
|
||||
<button id="hint-btn">Hint</button>
|
||||
</div>
|
||||
<div id="board-viewport">
|
||||
<div id="game-board"></div>
|
||||
<img id="grayscale-reveal" class="reveal-img" src="" alt="">
|
||||
<img id="original-reveal" class="reveal-img" src="" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" style="text-align:center;">
|
||||
<ins class="adsbygoogle"
|
||||
style="display:block"
|
||||
data-ad-client="ca-pub-9504446465764716"
|
||||
data-ad-slot="5334609005"
|
||||
data-ad-format="auto"
|
||||
data-full-width-responsive="true"></ins>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
const puzzleData = /*[[${puzzle}]]*/ null;
|
||||
/*]]>*/
|
||||
|
||||
if (puzzleData) {
|
||||
window.pageContext = {
|
||||
pageType: 'game',
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* ==============================================
|
||||
* nonogram.js (게임 플레이 로직)
|
||||
* (★ 리팩토링: 타이머 및 랭킹 등록 기능 추가)
|
||||
* ==============================================
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 백엔드(Thymeleaf)로부터 puzzleData가 제대로 전달되었는지 확인
|
||||
// 이 변수는 nonogram.html에만 존재하므로, upload.html에서는 이 로직이 실행되지 않음.
|
||||
if (typeof puzzleData === 'undefined' || !puzzleData) {
|
||||
// game-board가 없는 upload.html에서는 오류를 뱉지 않고 조용히 종료됨.
|
||||
const gb = document.getElementById('game-board');
|
||||
if (gb) {
|
||||
gb.innerHTML = '<h2>오류: 퍼즐 데이터를 불러올 수 없습니다.</h2>';
|
||||
}
|
||||
return; // upload.html에서는 여기서 즉시 return됨.
|
||||
}
|
||||
|
||||
// --- 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');
|
||||
const resultOverlay = document.getElementById('result-overlay');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const modalMessage = document.getElementById('modal-message');
|
||||
const modalButtons = document.getElementById('modal-buttons');
|
||||
|
||||
|
||||
// --- (★ 수정) 게임 상태 변수 (타이머 추가) ---
|
||||
let currentMode = 'fill';
|
||||
let points = 5;
|
||||
let isGameFinished = false;
|
||||
let gameStartTime = 0; // (★ 신규) 게임 시작 시간 (ms)
|
||||
|
||||
let isDragging = false;
|
||||
let dragAction = null;
|
||||
let startCell = null;
|
||||
let lastHoveredCell = null;
|
||||
let currentSelection = new Set();
|
||||
let affectedRows = new Set();
|
||||
let affectedCols = new Set();
|
||||
|
||||
// --- 퍼즐 데이터 및 플레이어 진행 상황 ---
|
||||
const solution = puzzleData.solutionGrid;
|
||||
const numRows = solution.length;
|
||||
const numCols = solution[0].length;
|
||||
let playerGrid = Array(numRows).fill(0).map(() => Array(numCols).fill(0));
|
||||
let lockedRows = Array(numRows).fill(false);
|
||||
let lockedCols = Array(numCols).fill(false);
|
||||
|
||||
|
||||
function updateMode() {
|
||||
currentMode = document.querySelector('input[name="play-mode"]:checked').value;
|
||||
}
|
||||
|
||||
function calculateCellSize() {
|
||||
// ... (셀 크기 계산 로직 - 수정 없음) ...
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.position = 'absolute';
|
||||
tempContainer.style.visibility = 'hidden';
|
||||
const tempCell = document.createElement('div');
|
||||
tempCell.className = 'clue-cell';
|
||||
tempCell.textContent = '0';
|
||||
tempContainer.appendChild(tempCell);
|
||||
document.body.appendChild(tempContainer);
|
||||
const fontHeight = tempCell.offsetHeight;
|
||||
tempCell.textContent = '10';
|
||||
const doubleDigitWidth = tempCell.offsetWidth;
|
||||
document.body.removeChild(tempContainer);
|
||||
const baseSize = Math.max(fontHeight, doubleDigitWidth, 30);
|
||||
return baseSize + 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* (★ 수정) drawBoard (타이머 시작점 추가)
|
||||
*/
|
||||
function drawBoard(cellSize) {
|
||||
// ... (모든 보드 그리기 DOM 생성 로직 - 수정 없음) ...
|
||||
gameBoard.style.gridTemplateColumns = `${cellSize * 2}px 1fr`;
|
||||
gameBoard.style.gridTemplateRows = `${cellSize * 2}px 1fr`;
|
||||
const corner = document.createElement('div');
|
||||
const colCluesContainer = document.createElement('div');
|
||||
colCluesContainer.className = 'col-clues-container';
|
||||
const rowCluesContainer = document.createElement('div');
|
||||
rowCluesContainer.className = 'row-clues-container';
|
||||
const puzzleGridContainer = document.createElement('div');
|
||||
puzzleGridContainer.className = 'puzzle-grid-container';
|
||||
puzzleGridContainer.style.gridTemplateColumns = `repeat(${numCols}, ${cellSize}px)`;
|
||||
puzzleGridContainer.style.gridTemplateRows = `repeat(${numRows}, ${cellSize}px)`;
|
||||
|
||||
puzzleData.colClues.forEach((clues, index) => {
|
||||
const clueCell = document.createElement('div');
|
||||
clueCell.className = 'clue-cell col-clue';
|
||||
clueCell.id = `col-clue-${index}`;
|
||||
clueCell.style.width = `${cellSize}px`;
|
||||
clueCell.innerHTML = clues.join('<br>');
|
||||
if ((index + 1) % 5 === 0 && index < numCols - 1) clueCell.classList.add('guide-line-right');
|
||||
colCluesContainer.appendChild(clueCell);
|
||||
});
|
||||
puzzleData.rowClues.forEach((clues, index) => {
|
||||
const clueCell = document.createElement('div');
|
||||
clueCell.className = 'clue-cell row-clue';
|
||||
clueCell.id = `row-clue-${index}`;
|
||||
clueCell.style.height = `${cellSize}px`;
|
||||
clueCell.textContent = clues.join(' ');
|
||||
if ((index + 1) % 5 === 0 && index < numRows - 1) clueCell.classList.add('guide-line-bottom');
|
||||
rowCluesContainer.appendChild(clueCell);
|
||||
});
|
||||
for (let r = 0; r < numRows; r++) {
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'grid-cell';
|
||||
cell.dataset.row = r;
|
||||
cell.dataset.col = c;
|
||||
if ((c + 1) % 5 === 0 && c < numCols - 1) cell.classList.add('guide-line-right');
|
||||
if ((r + 1) % 5 === 0 && r < numRows - 1) cell.classList.add('guide-line-bottom');
|
||||
puzzleGridContainer.appendChild(cell);
|
||||
}
|
||||
}
|
||||
gameBoard.appendChild(corner);
|
||||
gameBoard.appendChild(colCluesContainer);
|
||||
gameBoard.appendChild(rowCluesContainer);
|
||||
gameBoard.appendChild(puzzleGridContainer);
|
||||
|
||||
// (★ 신규) 보드가 그려지는 시점을 게임 시작 시간으로 기록
|
||||
gameStartTime = Date.now();
|
||||
|
||||
attachEventListeners(puzzleGridContainer);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 화면 너비에 맞춰 게임 보드 전체를 비율 그대로 축소/확대합니다.
|
||||
*/
|
||||
function fitBoardToScreen() {
|
||||
const viewport = document.getElementById('board-viewport');
|
||||
const board = document.getElementById('game-board');
|
||||
board.style.transform = 'scale(1)';
|
||||
const boardRect = board.getBoundingClientRect();
|
||||
const viewportRect = viewport.getBoundingClientRect();
|
||||
if (boardRect.width > viewportRect.width) {
|
||||
const scale = viewportRect.width / boardRect.width;
|
||||
board.style.transform = `scale(${scale})`;
|
||||
viewport.style.height = `${boardRect.height * scale}px`;
|
||||
} else {
|
||||
board.style.transform = 'scale(1)';
|
||||
viewport.style.height = `${boardRect.height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 셀의 상태를 변경하는 유일한 함수. 모든 사용자 입력은 이 함수를 거칩니다.
|
||||
*/
|
||||
function updateCellState(cell, action) {
|
||||
if (isGameFinished) return;
|
||||
const row = parseInt(cell.dataset.row);
|
||||
const col = parseInt(cell.dataset.col);
|
||||
if (lockedRows[row] || lockedCols[col]) return;
|
||||
affectedRows.add(row);
|
||||
affectedCols.add(col);
|
||||
const currentState = playerGrid[row][col];
|
||||
let newState = currentState;
|
||||
if (action === 'fill') {
|
||||
if (solution[row][col] === 0) {
|
||||
points--;
|
||||
updatePointsDisplay();
|
||||
cell.classList.add('incorrect');
|
||||
setTimeout(() => cell.classList.remove('incorrect'), 500);
|
||||
if (points <= 0) triggerGameOver();
|
||||
return;
|
||||
}
|
||||
newState = 1;
|
||||
} else if (action === 'mark') {
|
||||
newState = -1;
|
||||
} else if (action === 'clear') {
|
||||
newState = 0;
|
||||
}
|
||||
if (currentState !== newState) {
|
||||
playerGrid[row][col] = newState;
|
||||
cell.classList.toggle('filled', newState === 1);
|
||||
cell.classList.toggle('marked', newState === -1);
|
||||
}
|
||||
}
|
||||
// --- (이벤트 리스너 및 드래그/터치 핸들러) ---
|
||||
// (★ 수정 없음) attachEventListeners, handleDragStart, handleDragMove, handleDragEnd
|
||||
// (★ 수정 없음) updateSelectionVisuals, clearSelectionVisuals
|
||||
// --- (모두 동일하게 유지) ---
|
||||
function attachEventListeners(grid) {
|
||||
grid.addEventListener('mousedown', (e) => handleDragStart(e));
|
||||
grid.addEventListener('mouseover', (e) => handleDragMove(e));
|
||||
grid.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
grid.addEventListener('touchstart', (e) => handleDragStart(e), { passive: false });
|
||||
grid.addEventListener('touchmove', (e) => handleDragMove(e), { passive: false });
|
||||
}
|
||||
window.addEventListener('mouseup', () => handleDragEnd());
|
||||
window.addEventListener('touchend', () => handleDragEnd());
|
||||
modeSelector.addEventListener('change', updateMode);
|
||||
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)];
|
||||
if (e.type === 'mousedown') {
|
||||
if (e.button === 0) {
|
||||
dragAction = (currentState === 1) ? 'clear' : 'fill';
|
||||
document.querySelector('input[name="play-mode"][value="fill"]').checked = true;
|
||||
} else if (e.button === 2) {
|
||||
dragAction = (currentState === -1) ? 'clear' : 'mark';
|
||||
document.querySelector('input[name="play-mode"][value="mark"]').checked = true;
|
||||
}
|
||||
} else {
|
||||
const currentMode = document.querySelector('input[name="play-mode"]:checked').value;
|
||||
if (currentMode === 'fill') {
|
||||
dragAction = (currentState === 1) ? 'clear' : 'fill';
|
||||
} else {
|
||||
dragAction = (currentState === -1) ? 'clear' : 'mark';
|
||||
}
|
||||
}
|
||||
startCell = { row: parseInt(cell.dataset.row), col: parseInt(cell.dataset.col) };
|
||||
lastHoveredCell = startCell;
|
||||
updateSelectionVisuals();
|
||||
}
|
||||
function handleDragMove(e) {
|
||||
if (!isDragging) return;
|
||||
e.preventDefault();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleDragEnd() {
|
||||
if (!isDragging) return;
|
||||
currentSelection.forEach(cell => updateCellState(cell, dragAction));
|
||||
clearSelectionVisuals();
|
||||
if (dragAction === 'fill' || dragAction === 'clear') {
|
||||
checkAndLockCompletedLines(affectedRows, affectedCols);
|
||||
}
|
||||
checkWinCondition();
|
||||
isDragging = false;
|
||||
dragAction = null;
|
||||
startCell = null;
|
||||
lastHoveredCell = null;
|
||||
currentSelection.clear();
|
||||
affectedRows.clear();
|
||||
affectedCols.clear();
|
||||
}
|
||||
/**
|
||||
* 드래그 중인 사각형 영역을 계산하고 시각적으로 업데이트하는 함수
|
||||
*/
|
||||
function updateSelectionVisuals() {
|
||||
const newSelection = new Set();
|
||||
if (!startCell || !lastHoveredCell) return;
|
||||
const r1 = Math.min(startCell.row, lastHoveredCell.row);
|
||||
const r2 = Math.max(startCell.row, lastHoveredCell.row);
|
||||
const c1 = Math.min(startCell.col, lastHoveredCell.col);
|
||||
const c2 = Math.max(startCell.col, lastHoveredCell.col);
|
||||
for (let r = r1; r <= r2; r++) {
|
||||
for (let c = c1; c <= c2; c++) {
|
||||
const cell = document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`);
|
||||
if (cell) newSelection.add(cell);
|
||||
}
|
||||
}
|
||||
currentSelection.forEach(cell => {
|
||||
if (!newSelection.has(cell)) cell.classList.remove('selecting');
|
||||
});
|
||||
newSelection.forEach(cell => {
|
||||
if (!currentSelection.has(cell)) cell.classList.add('selecting');
|
||||
});
|
||||
currentSelection = newSelection;
|
||||
}
|
||||
/**
|
||||
* 모든 시각적 피드백을 제거하는 함수
|
||||
*/
|
||||
function clearSelectionVisuals() {
|
||||
currentSelection.forEach(cell => cell.classList.remove('selecting'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 행이 '칠해야 할 칸'을 모두 만족했는지 검사
|
||||
*/
|
||||
function isRowComplete(rowIndex) {
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
if (solution[rowIndex][c] === 1 && playerGrid[rowIndex][c] !== 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 특정 열이 '칠해야 할 칸'을 모두 만족했는지 검사
|
||||
*/
|
||||
function isColComplete(colIndex) {
|
||||
for (let r = 0; r < numRows; r++) {
|
||||
if (solution[r][colIndex] === 1 && playerGrid[r][colIndex] !== 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- (게임 완료 체크 로직) ---
|
||||
/**
|
||||
* 완성된 라인을 확인하고 잠금 처리 및 스타일 변경 (X 자동 완성 없음)
|
||||
*/
|
||||
function checkAndLockCompletedLines(rowsToCheck, colsToCheck) {
|
||||
rowsToCheck.forEach(r => {
|
||||
if (!lockedRows[r] && isRowComplete(r)) {
|
||||
lockedRows[r] = true;
|
||||
document.getElementById(`row-clue-${r}`).classList.add('completed');
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
|
||||
}
|
||||
}
|
||||
});
|
||||
colsToCheck.forEach(c => {
|
||||
if (!lockedCols[c] && isColComplete(c)) {
|
||||
lockedCols[c] = true;
|
||||
document.getElementById(`col-clue-${c}`).classList.add('completed');
|
||||
for (let r = 0; r < numRows; r++) {
|
||||
document.querySelector(`.grid-cell[data-row='${r}'][data-col='${c}']`).classList.add('locked');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function checkWinCondition() {
|
||||
if (isGameFinished) return;
|
||||
for (let r = 0; r < numRows; r++) {
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
const playerState = (playerGrid[r][c] === 1) ? 1 : 0;
|
||||
if (playerState !== solution[r][c]) return;
|
||||
}
|
||||
}
|
||||
triggerGameSuccess();
|
||||
}
|
||||
|
||||
|
||||
function updatePointsDisplay() {
|
||||
pointsDisplay.textContent = points;
|
||||
hintBtn.disabled = (points <= 0 || isGameFinished);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 게임 실패 처리
|
||||
*/
|
||||
function triggerGameOver() {
|
||||
if (isGameFinished) return;
|
||||
isGameFinished = true;
|
||||
document.querySelector('.puzzle-grid-container').style.pointerEvents = 'none';
|
||||
hintBtn.disabled = true;
|
||||
showGameSuccessModal({
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id,
|
||||
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
|
||||
primaryScore: completionTimeSeconds,
|
||||
secondaryScore: hintsUsed
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* (★ 수정) 게임 성공 처리 (타이머 계산 및 랭킹 버튼 추가)
|
||||
*/
|
||||
function triggerGameSuccess() {
|
||||
if (isGameFinished) return;
|
||||
isGameFinished = true;
|
||||
|
||||
// (★ 신규) 게임 완료 시간 및 힌트 사용량 계산
|
||||
const completionTimeSeconds = Math.floor((Date.now() - gameStartTime) / 1000);
|
||||
const hintsUsed = 5 - points; // 힌트 사용량 = 5 - 남은 포인트
|
||||
|
||||
// --- 요소 참조 및 상호작용 비활성화 ---
|
||||
const viewport = document.getElementById('board-viewport');
|
||||
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;
|
||||
|
||||
// --- 애니메이션 위치 및 크기 계산 ---
|
||||
const gridRect = puzzleGridContainer.getBoundingClientRect();
|
||||
const viewportRect = viewport.getBoundingClientRect();
|
||||
const top = gridRect.top - viewportRect.top;
|
||||
const left = gridRect.left - viewportRect.left; // (오타 수정) viewportRect.top -> viewportRect.left
|
||||
|
||||
[grayscaleImg, originalImg].forEach(img => {
|
||||
img.style.top = `${top}px`;
|
||||
img.style.left = `${left}px`;
|
||||
img.style.width = `${gridRect.width}px`;
|
||||
img.style.height = `${gridRect.height}px`;
|
||||
// [수정] Base64 대신 URL 경로를 사용하도록 변경
|
||||
img.src = (img.id === 'grayscale-reveal')
|
||||
? `/puzzle/images/${puzzleData.grayscaleImageFile}`
|
||||
: `/puzzle/images/${puzzleData.originalImageFile}`;
|
||||
});
|
||||
|
||||
// --- 애니메이션 순차 실행 ---
|
||||
setTimeout(() => {
|
||||
grayscaleImg.style.opacity = '1';
|
||||
setTimeout(() => {
|
||||
originalImg.style.opacity = '1';
|
||||
setTimeout(() => {
|
||||
// (★ 수정) 모달 버튼 설정에 "랭킹 등록" 버튼 추가
|
||||
showGameSuccessModal({
|
||||
gameType: 'NONOGRAM',
|
||||
contextId: puzzleData.id,
|
||||
successMessage: `퍼즐 완성! (시간: ${completionTimeSeconds}초, 힌트 사용: ${hintsUsed}개)`,
|
||||
primaryScore: completionTimeSeconds,
|
||||
secondaryScore: hintsUsed
|
||||
});
|
||||
}, 2000);
|
||||
}, 2000);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 힌트 버튼 클릭 이벤트 처리
|
||||
hintBtn.addEventListener('click', () => {
|
||||
if (points <= 0 || isGameFinished) return;
|
||||
points--;
|
||||
updatePointsDisplay();
|
||||
const hintCandidates = [];
|
||||
for (let r = 0; r < numRows; r++) {
|
||||
for (let c = 0; c < numCols; c++) {
|
||||
if (solution[r][c] === 1 && playerGrid[r][c] !== 1) {
|
||||
hintCandidates.push({ r, c });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hintCandidates.length > 0) {
|
||||
const hint = hintCandidates[Math.floor(Math.random() * hintCandidates.length)];
|
||||
const cellToReveal = document.querySelector(`.grid-cell[data-row='${hint.r}'][data-col='${hint.c}']`);
|
||||
|
||||
updateCellState(cellToReveal, 'fill');
|
||||
|
||||
const hintAffectedRows = new Set([hint.r]);
|
||||
const hintAffectedCols = new Set([hint.c]);
|
||||
checkAndLockCompletedLines(hintAffectedRows, hintAffectedCols);
|
||||
checkWinCondition();
|
||||
} else {
|
||||
showAlert("알림","더 이상 사용할 힌트가 없습니다!");
|
||||
points++;
|
||||
updatePointsDisplay();
|
||||
}
|
||||
if (points <= 0 && !isGameFinished) {
|
||||
triggerGameOver();
|
||||
}
|
||||
});
|
||||
|
||||
// --- 초기 실행 ---
|
||||
const optimalCellSize = calculateCellSize();
|
||||
drawBoard(optimalCellSize);
|
||||
updatePointsDisplay();
|
||||
updateMode();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
fitBoardToScreen();
|
||||
window.addEventListener('resize', fitBoardToScreen);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* ==============================================
|
||||
* upload.js (업로드 페이지 로직)
|
||||
* (★ 리팩토링: 통합 API 경로 사용)
|
||||
* ==============================================
|
||||
*/
|
||||
let currentPuzzleData = null; // 업로드 성공 시 퍼즐 데이터 저장
|
||||
|
||||
|
||||
|
||||
// (★ 수정 없음) 업로드 페이지용 퍼즐 미리보기 그리기 함수
|
||||
function drawPuzzle(puzzleData) {
|
||||
const container = document.getElementById('puzzle-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
const { solutionGrid, rowClues, colClues } = puzzleData;
|
||||
const numRows = solutionGrid.length;
|
||||
const numCols = solutionGrid[0].length;
|
||||
|
||||
container.style.gridTemplateColumns = `auto repeat(${numCols}, 1fr)`;
|
||||
container.style.gridTemplateRows = `auto repeat(${numRows}, 1fr)`;
|
||||
|
||||
// 1. 코너
|
||||
const corner = document.createElement('div');
|
||||
corner.className = 'grid-cell';
|
||||
container.appendChild(corner);
|
||||
|
||||
// 2. 열 힌트
|
||||
for (const clues of colClues) {
|
||||
const clueCell = document.createElement('div');
|
||||
clueCell.className = 'clue-cell';
|
||||
clueCell.innerHTML = clues.join('<br>');
|
||||
container.appendChild(clueCell);
|
||||
}
|
||||
|
||||
// 3. 행 힌트 및 정답 그리드
|
||||
for (let i = 0; i < numRows; i++) {
|
||||
const rowClueCell = document.createElement('div');
|
||||
rowClueCell.className = 'clue-cell';
|
||||
rowClueCell.textContent = rowClues[i].join(' ');
|
||||
container.appendChild(rowClueCell);
|
||||
|
||||
for (let j = 0; j < numCols; j++) {
|
||||
const cell = document.createElement('div');
|
||||
cell.className = 'solution-cell';
|
||||
if (solutionGrid[i][j] === 1) {
|
||||
cell.classList.add('filled');
|
||||
} else {
|
||||
cell.classList.add('empty');
|
||||
}
|
||||
container.appendChild(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.puzzleData = /*[[${puzzle}]]*/ null;
|
||||
</script>
|
||||
<script type="module" th:src="@{/js/pages/game_nonogram.js}"></script>
|
||||
</th:block>
|
||||
</html>
|
||||
Reference in New Issue
Block a user