544 lines
20 KiB
PHP
544 lines
20 KiB
PHP
<?php
|
|
session_start();
|
|
|
|
// Gatekeeper: redirect if not logged in
|
|
if (!isset($_SESSION['user_id'])) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
ini_set('display_errors', 1);
|
|
error_reporting(E_ALL);
|
|
|
|
require_once 'db/config.php';
|
|
|
|
// Handle Score Submission
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_score') {
|
|
header('Content-Type: application/json');
|
|
$user_id = $_SESSION['user_id'];
|
|
$puzzle_id = (int)$_POST['puzzle_id'];
|
|
$time_taken = (int)$_POST['time_taken'];
|
|
$moves = (int)$_POST['moves'];
|
|
|
|
// Simple scoring formula
|
|
$score = max(0, 10000 - ($time_taken * 10) - ($moves * 5));
|
|
|
|
try {
|
|
$pdo = db();
|
|
$stmt = $pdo->prepare("INSERT INTO scores (user_id, puzzle_id, time_taken, moves, score) VALUES (?, ?, ?, ?, ?)");
|
|
$stmt->execute([$user_id, $puzzle_id, $time_taken, $moves, $score]);
|
|
echo json_encode(['success' => true, 'score' => $score]);
|
|
} catch (PDOException $e) {
|
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
}
|
|
exit; // Important: stop script execution after AJAX response
|
|
}
|
|
|
|
|
|
// --- VARIABLES ---
|
|
$puzzle_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
|
|
$difficulty = isset($_GET['difficulty']) ? (int)$_GET['difficulty'] : 16;
|
|
|
|
// Determine grid size based on difficulty
|
|
$valid_difficulties = [
|
|
16 => [4, 4], // 4x4 grid
|
|
32 => [8, 4], // 8x4 grid
|
|
64 => [8, 8] // 8x8 grid
|
|
];
|
|
if (!isset($valid_difficulties[$difficulty])) {
|
|
$difficulty = 16; // Fallback to default if invalid
|
|
}
|
|
list($cols, $rows) = $valid_difficulties[$difficulty];
|
|
|
|
$puzzle = null;
|
|
$error_message = '';
|
|
$pieces = [];
|
|
$source_width = 1;
|
|
$source_height = 1;
|
|
|
|
// --- DATA FETCHING & PUZZLE CREATION ---
|
|
if ($puzzle_id > 0) {
|
|
try {
|
|
$pdo = db();
|
|
$stmt = $pdo->prepare("SELECT * FROM puzzles WHERE id = ?");
|
|
$stmt->execute([$puzzle_id]);
|
|
$puzzle = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$puzzle) {
|
|
$error_message = "Puzzle non trovato.";
|
|
} else {
|
|
$image_path = 'uploads/' . $puzzle['file_name'];
|
|
if (!file_exists($image_path)) {
|
|
$error_message = "File immagine del puzzle non trovato.";
|
|
} else {
|
|
$image_info = getimagesize($image_path);
|
|
$source_width = $image_info[0];
|
|
$source_height = $image_info[1];
|
|
|
|
$pieces_dir = sprintf('puzzles/%d/%d', $puzzle['id'], $difficulty);
|
|
if (!is_dir($pieces_dir)) {
|
|
mkdir($pieces_dir, 0777, true);
|
|
}
|
|
|
|
$piece_files = glob($pieces_dir . '/piece_*.jpg');
|
|
|
|
if (count($piece_files) !== ($cols * $rows)) {
|
|
foreach ($piece_files as $file) { unlink($file); }
|
|
|
|
$mime_type = $image_info['mime'];
|
|
$source_image = null;
|
|
switch ($mime_type) {
|
|
case 'image/jpeg': $source_image = imagecreatefromjpeg($image_path); break;
|
|
case 'image/png': $source_image = imagecreatefrompng($image_path); break;
|
|
case 'image/gif': $source_image = imagecreatefromgif($image_path); break;
|
|
default: $error_message = "Formato immagine non supportato."; break;
|
|
}
|
|
|
|
if ($source_image) {
|
|
$piece_width = floor($source_width / $cols);
|
|
$piece_height = floor($source_height / $rows);
|
|
|
|
for ($y = 0; $y < $rows; $y++) {
|
|
for ($x = 0; $x < $cols; $x++) {
|
|
$piece = imagecreatetruecolor($piece_width, $piece_height);
|
|
imagecopy($piece, $source_image, 0, 0, $x * $piece_width, $y * $piece_height, $piece_width, $piece_height);
|
|
imagejpeg($piece, "{$pieces_dir}/piece_{$y}_{$x}.jpg", 95);
|
|
imagedestroy($piece);
|
|
}
|
|
}
|
|
imagedestroy($source_image);
|
|
}
|
|
}
|
|
|
|
$pieces = glob($pieces_dir . '/piece_*.jpg');
|
|
shuffle($pieces);
|
|
}
|
|
}
|
|
} catch (PDOException $e) {
|
|
$error_message = "Errore di sistema: " . $e->getMessage();
|
|
}
|
|
} else {
|
|
$error_message = "ID del puzzle non valido.";
|
|
}
|
|
|
|
$page_title = 'Risolvi: ' . ($puzzle ? htmlspecialchars($puzzle['name']) : 'Puzzle');
|
|
require_once 'includes/header.php';
|
|
?>
|
|
|
|
<style>
|
|
:root {
|
|
--puzzle-width: clamp(500px, 70vw, 800px);
|
|
--puzzle-background: #f0f0f0;
|
|
--piece-tray-height: 75vh;
|
|
--piece-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
--dragging-shadow: 0 10px 25px rgba(0,0,0,0.4);
|
|
--drop-zone-border: 1px dashed #aaa;
|
|
--drop-zone-near-bg: rgba(40, 167, 69, 0.25);
|
|
--drop-zone-near-border: #28a745;
|
|
--drop-zone-near-shadow: inset 0 0 10px rgba(40, 167, 69, 0.4);
|
|
}
|
|
|
|
#puzzle-board {
|
|
display: grid;
|
|
border: 2px solid #333;
|
|
background-color: var(--puzzle-background);
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
|
width: var(--puzzle-width);
|
|
margin: 2rem auto;
|
|
position: relative;
|
|
}
|
|
|
|
#pieces-tray {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
align-content: flex-start;
|
|
height: var(--piece-tray-height);
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
background: #f9f9f9;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.puzzle-piece {
|
|
border: 1px solid #ccc;
|
|
box-shadow: var(--piece-shadow);
|
|
border-radius: 3px;
|
|
cursor: grab;
|
|
/* Using touch-action to prevent scrolling on touch devices when dragging */
|
|
touch-action: none;
|
|
position: relative;
|
|
z-index: 1;
|
|
transition: box-shadow 0.2s, transform 0.2s;
|
|
}
|
|
|
|
/* --- POINTER EVENTS DRAG & DROP SYSTEM --- */
|
|
.puzzle-piece.is-dragging {
|
|
position: fixed;
|
|
cursor: grabbing;
|
|
opacity: 0.9;
|
|
transform: scale(1.05) rotate(2deg);
|
|
box-shadow: var(--dragging-shadow);
|
|
z-index: 1000;
|
|
/* CRITICAL: Prevents the dragged element from blocking pointer events to what's underneath (the drop zones) */
|
|
pointer-events: none;
|
|
}
|
|
|
|
.drop-zone {
|
|
border: var(--drop-zone-border);
|
|
background-color: rgba(0,0,0,0.02);
|
|
transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s;
|
|
box-sizing: border-box;
|
|
position: relative;
|
|
}
|
|
|
|
.drop-zone.near {
|
|
background-color: var(--drop-zone-near-bg);
|
|
border-color: var(--drop-zone-near-border);
|
|
border-style: solid;
|
|
box-shadow: var(--drop-zone-near-shadow);
|
|
}
|
|
|
|
.puzzle-piece.snapped {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
box-shadow: none;
|
|
border: none;
|
|
border-radius: 0;
|
|
cursor: default;
|
|
transform: none;
|
|
z-index: 0;
|
|
touch-action: auto;
|
|
}
|
|
</style>
|
|
|
|
<!-- Completion Modal -->
|
|
<div id="win-modal" class="modal-backdrop hidden">
|
|
<div class="modal-content">
|
|
<button id="close-modal-btn" class="close-btn">×</button>
|
|
<h2>Complimenti!</h2>
|
|
<p>Hai risolto il puzzle!</p>
|
|
<p class="score">Punteggio: <strong id="final-score">0</strong></p>
|
|
<div class="modal-actions">
|
|
<a href="leaderboard.php" class="btn btn-primary">Classifica</a>
|
|
<a href="index.php" class="btn btn-secondary">Gioca Ancora</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main class="container mt-4">
|
|
<div class="text-center mb-4">
|
|
<h1><?php echo $puzzle ? htmlspecialchars($puzzle['name']) : 'Risolvi il Puzzle'; ?></h1>
|
|
<div class="d-flex justify-content-center align-items-center gap-3">
|
|
<a href="index.php" class="btn btn-secondary btn-sm">Torna alla Galleria</a>
|
|
<span class="badge bg-primary">Tempo: <span id="timer">0s</span></span>
|
|
<span class="badge bg-info">Mosse: <span id="move-counter">0</span></span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($error_message): ?>
|
|
<div class="alert alert-danger"><?php echo htmlspecialchars($error_message); ?></div>
|
|
<?php elseif ($puzzle): ?>
|
|
<div class="text-center mb-3">
|
|
<form method="GET" action="puzzle.php" class="d-inline-flex align-items-center">
|
|
<input type="hidden" name="id" value="<?php echo $puzzle_id; ?>">
|
|
<label for="difficulty" class="form-label me-2 mb-0">Difficoltà:</label>
|
|
<select name="difficulty" id="difficulty" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<option value="16" <?php if ($difficulty == 16) echo 'selected'; ?>>Facile (16 pezzi)</option>
|
|
<option value="32" <?php if ($difficulty == 32) echo 'selected'; ?>>Medio (32 pezzi)</option>
|
|
<option value="64" <?php if ($difficulty == 64) echo 'selected'; ?>>Difficile (64 pezzi)</option>
|
|
</select>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8 col-md-12 mb-3">
|
|
<div id="puzzle-board" class="puzzle-board shadow-lg rounded"></div>
|
|
</div>
|
|
<div class="col-lg-4 col-md-12">
|
|
<div class="card p-2">
|
|
<h2 class="h5 text-center">I Tuoi Pezzi</h2>
|
|
<div id="pieces-tray" class="pieces-tray p-2">
|
|
<?php
|
|
foreach ($pieces as $i => $piece_path) {
|
|
preg_match('/piece_(\d+)_(\d+)\.jpg$/', $piece_path, $matches);
|
|
$row = $matches[1];
|
|
$col = $matches[2];
|
|
echo sprintf(
|
|
'<img src="%s" id="piece-%d" class="puzzle-piece" alt="Pezzo del puzzle" draggable="true" data-position="%s">',
|
|
htmlspecialchars($piece_path),
|
|
$i,
|
|
"{$row}-{$col}"
|
|
);
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- ELEMENTI DEL DOM ---
|
|
const board = document.getElementById('puzzle-board');
|
|
const piecesTray = document.getElementById('pieces-tray');
|
|
const winModal = document.getElementById('win-modal');
|
|
const closeModalBtn = document.getElementById('close-modal-btn');
|
|
const finalScoreEl = document.getElementById('final-score');
|
|
const timerEl = document.getElementById('timer');
|
|
const moveCounterEl = document.getElementById('move-counter');
|
|
|
|
// --- COSTANTI DI GIOCO ---
|
|
const PUZZLE_ID = <?php echo $puzzle_id; ?>;
|
|
const ORIGINAL_IMAGE_PATH = 'uploads/<?php echo $puzzle['file_name']; ?>';
|
|
const COLS = <?php echo $cols; ?>;
|
|
const ROWS = <?php echo $rows; ?>;
|
|
const SNAP_RADIUS = 30; // Increased radius for better usability
|
|
|
|
// --- STATO DI GIOCO ---
|
|
let moves = 0;
|
|
let gameStarted = false;
|
|
let startTime = 0;
|
|
let timerInterval = null;
|
|
let dropZones = [];
|
|
let pieces = [];
|
|
|
|
// =========================================================================
|
|
// --- NUOVO SISTEMA DRAG & DROP BASATO SU POINTER EVENTS API ---
|
|
// =========================================================================
|
|
const dragController = {
|
|
isDragging: false,
|
|
dragElement: null, // L'elemento DOM trascinato
|
|
pieceData: null, // L'oggetto pezzo con i suoi dati
|
|
offsetX: 0,
|
|
offsetY: 0,
|
|
lastPointerX: 0,
|
|
lastPointerY: 0,
|
|
animationFrameId: null,
|
|
|
|
// Inizializza l'intero sistema
|
|
initialize() {
|
|
// Eventi universali per mouse e touch
|
|
document.addEventListener('pointerdown', this.handlePointerDown.bind(this));
|
|
document.addEventListener('pointermove', this.handlePointerMove.bind(this), { passive: false });
|
|
document.addEventListener('pointerup', this.handlePointerUp.bind(this));
|
|
|
|
// Previene comportamenti di default che interferiscono
|
|
document.addEventListener('selectstart', (e) => {
|
|
if (this.isDragging) e.preventDefault();
|
|
});
|
|
},
|
|
|
|
// --- GESTIONE EVENTI POINTER ---
|
|
handlePointerDown(e) {
|
|
const target = e.target.closest('.puzzle-piece');
|
|
if (!target || target.classList.contains('snapped') || this.isDragging) return;
|
|
|
|
if (!gameStarted) startTimer();
|
|
|
|
this.isDragging = true;
|
|
this.dragElement = target;
|
|
this.pieceData = pieces.find(p => p.element === target);
|
|
|
|
const rect = this.dragElement.getBoundingClientRect();
|
|
this.offsetX = e.clientX - rect.left;
|
|
this.offsetY = e.clientY - rect.top;
|
|
|
|
this.dragElement.classList.add('is-dragging');
|
|
|
|
// Posiziona l'elemento per il trascinamento
|
|
this.updatePosition(e.clientX, e.clientY);
|
|
},
|
|
|
|
handlePointerMove(e) {
|
|
if (!this.isDragging) return;
|
|
e.preventDefault(); // Previene lo scroll su mobile
|
|
|
|
this.lastPointerX = e.clientX;
|
|
this.lastPointerY = e.clientY;
|
|
|
|
// Usa rAF per un'animazione fluida e performante
|
|
if (!this.animationFrameId) {
|
|
this.animationFrameId = requestAnimationFrame(() => {
|
|
this.updatePosition(this.lastPointerX, this.lastPointerY);
|
|
this.checkNearDropZone();
|
|
this.animationFrameId = null;
|
|
});
|
|
}
|
|
},
|
|
|
|
handlePointerUp(e) {
|
|
if (!this.isDragging) return;
|
|
|
|
if (this.animationFrameId) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
}
|
|
|
|
const correctDropZone = dropZones.find(zone => zone.dataset.position === this.pieceData.correctPos);
|
|
let snapped = false;
|
|
|
|
// Logica di Snap
|
|
if (correctDropZone && correctDropZone.classList.contains('near') && !correctDropZone.hasChildNodes()) {
|
|
correctDropZone.appendChild(this.dragElement);
|
|
this.dragElement.classList.add('snapped');
|
|
this.pieceData.isSnapped = true;
|
|
snapped = true;
|
|
incrementMoves();
|
|
checkWin();
|
|
}
|
|
|
|
// Ritorno al vassoio se non agganciato
|
|
if (!snapped) {
|
|
piecesTray.appendChild(this.dragElement);
|
|
}
|
|
|
|
// Pulizia finale
|
|
this.dragElement.classList.remove('is-dragging');
|
|
this.dragElement.style.transform = ''; // Resetta la trasformazione
|
|
dropZones.forEach(zone => zone.classList.remove('near'));
|
|
|
|
// Resetta lo stato del controller
|
|
this.isDragging = false;
|
|
this.dragElement = null;
|
|
this.pieceData = null;
|
|
},
|
|
|
|
// --- FUNZIONI AUSILIARIE ---
|
|
updatePosition(x, y) {
|
|
if (!this.dragElement) return;
|
|
const newX = x - this.offsetX;
|
|
const newY = y - this.offsetY;
|
|
// Applica la trasformazione per muovere l'elemento
|
|
this.dragElement.style.transform = `translate(${newX}px, ${newY}px) scale(1.05) rotate(2deg)`;
|
|
},
|
|
|
|
checkNearDropZone() {
|
|
if (!this.dragElement) return;
|
|
|
|
const pieceRect = this.dragElement.getBoundingClientRect();
|
|
const pieceCenterX = pieceRect.left + pieceRect.width / 2;
|
|
const pieceCenterY = pieceRect.top + pieceRect.height / 2;
|
|
|
|
dropZones.forEach(zone => {
|
|
if (zone.dataset.position === this.pieceData.correctPos) {
|
|
const zoneRect = zone.getBoundingClientRect();
|
|
const zoneCenterX = zoneRect.left + zoneRect.width / 2;
|
|
const zoneCenterY = zoneRect.top + zoneRect.height / 2;
|
|
|
|
const distance = Math.hypot(pieceCenterX - zoneCenterX, pieceCenterY - zoneCenterY);
|
|
|
|
if (distance < SNAP_RADIUS) {
|
|
zone.classList.add('near');
|
|
} else {
|
|
zone.classList.remove('near');
|
|
}
|
|
} else {
|
|
zone.classList.remove('near');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// --- IMPOSTAZIONE INIZIALE DEL GIOCO ---
|
|
function initializeGame() {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
board.style.aspectRatio = `${img.naturalWidth} / ${img.naturalHeight}`;
|
|
board.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
|
board.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
|
|
|
|
createDropZones();
|
|
createPieces();
|
|
dragController.initialize(); // Avvia il controller del drag & drop
|
|
};
|
|
img.src = ORIGINAL_IMAGE_PATH;
|
|
}
|
|
|
|
function createDropZones() {
|
|
board.innerHTML = '';
|
|
dropZones = [];
|
|
for (let y = 0; y < ROWS; y++) {
|
|
for (let x = 0; x < COLS; x++) {
|
|
const dropZone = document.createElement('div');
|
|
dropZone.classList.add('drop-zone');
|
|
dropZone.dataset.position = `${y}-${x}`;
|
|
board.appendChild(dropZone);
|
|
dropZones.push(dropZone);
|
|
}
|
|
}
|
|
}
|
|
|
|
function createPieces() {
|
|
const allPieceElements = document.querySelectorAll('.puzzle-piece');
|
|
const trayPieceWidth = Math.min(120, (piecesTray.clientWidth - 40) / 4); // Adatta la larghezza nel vassoio
|
|
allPieceElements.forEach(element => {
|
|
const piece = {
|
|
element: element,
|
|
id: element.id,
|
|
correctPos: element.dataset.position,
|
|
isSnapped: false
|
|
};
|
|
piece.element.style.width = `${trayPieceWidth}px`;
|
|
piece.element.style.height = 'auto';
|
|
pieces.push(piece);
|
|
});
|
|
}
|
|
|
|
// --- LOGICA DI GIOCO (TIMER, MOSSE, VITTORIA) ---
|
|
const startTimer = () => {
|
|
if (gameStarted) return;
|
|
gameStarted = true;
|
|
startTime = Date.now();
|
|
timerInterval = setInterval(() => {
|
|
timerEl.textContent = `${Math.floor((Date.now() - startTime) / 1000)}s`;
|
|
}, 1000);
|
|
};
|
|
const incrementMoves = () => moveCounterEl.textContent = ++moves;
|
|
|
|
function checkWin() {
|
|
const isComplete = pieces.every(p => p.isSnapped);
|
|
if (isComplete) {
|
|
clearInterval(timerInterval);
|
|
const timeTaken = Math.floor((Date.now() - startTime) / 1000);
|
|
saveScoreAndShowModal(timeTaken);
|
|
}
|
|
}
|
|
|
|
function saveScoreAndShowModal(timeTaken) {
|
|
const formData = new FormData();
|
|
formData.append('action', 'save_score');
|
|
formData.append('puzzle_id', PUZZLE_ID);
|
|
formData.append('time_taken', timeTaken);
|
|
formData.append('moves', moves);
|
|
|
|
fetch('puzzle.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
finalScoreEl.textContent = data.success ? data.score : 'N/A';
|
|
winModal.classList.remove('hidden');
|
|
})
|
|
.catch(error => {
|
|
console.error('Error saving score:', error);
|
|
finalScoreEl.textContent = 'Errore';
|
|
winModal.classList.remove('hidden');
|
|
});
|
|
}
|
|
|
|
closeModalBtn.addEventListener('click', () => winModal.classList.add('hidden'));
|
|
winModal.addEventListener('click', e => {
|
|
if (e.target === winModal) winModal.classList.add('hidden');
|
|
});
|
|
|
|
// --- AVVIO GIOCO ---
|
|
initializeGame();
|
|
});
|
|
</script>
|
|
|
|
<?php require_once 'includes/footer.php'; ?>
|