34464-vm/puzzle.php
2025-09-29 00:35:53 +00:00

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">&times;</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'; ?>