diff --git a/assets/media/img/titolo.png b/assets/media/img/titolo.png new file mode 100755 index 0000000..ff979ea Binary files /dev/null and b/assets/media/img/titolo.png differ diff --git a/assets/models/tree.glb b/assets/models/tree.glb new file mode 100644 index 0000000..37f361d Binary files /dev/null and b/assets/models/tree.glb differ diff --git a/css/fase1.css b/css/fase1.css index 709990e..5509be5 100755 --- a/css/fase1.css +++ b/css/fase1.css @@ -65,7 +65,7 @@ body { /* Queste 4 righe garantiscono la centratura totale */ display: flex; flex-direction: column; - justify-content: center; + justify-content: center; align-items: center; z-index: 100; /* Assicurati che sia sopra al renderer di Three.js */ } @@ -110,10 +110,11 @@ body { } .main-title { - max-width: 80%; + width: 25%; height: auto; } + /* Divisione 50% Sotto */ .bottom-section { flex: 1; @@ -132,37 +133,72 @@ body { text-align: center; } -/* Styling dei Controlli (KBD) */ +/* Styling dei Controlli */ .controls-box { width: 100%; display: flex; flex-direction: column; align-items: center; -} -.controls-box h3 { - margin-bottom: 10px; - font-size: 1.2rem; - text-transform: uppercase; -} -.controls-box h3, .controls-box p, .pulse-text { - margin-top: 0; + justify-content: center; + height: 100%; + color: white; + text-shadow: 2px 2px 4px rgba(0,0,0,0.8); + font-family: 'Press Start 2P', cursive; } -kbd { - background-color: #eee; - border-radius: 3px; - border: 1px solid #b4b4b4; - box-shadow: 0 1px 1px rgba(0,0,0,0.2), 0 2px 0 0 rgba(255,255,255,0.7) inset; - color: #333; - display: inline-block; - font-size: 0.85em; - font-weight: 700; - line-height: 1; - padding: 2px 4px; - white-space: nowrap; - margin: 0 2px; +.controls-box h3 { + margin-bottom: 10px; + font-size: 1.4rem; + text-transform: uppercase; + text-align: center; } +.controls-box p { + margin: 6px 0; + font-size: 1.1rem; + text-align: center; +} + +/* Stile dei tasti */ +.controls-box kbd { + background-color: #5e5d5d; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-family: monospace; + font-weight: bold; + margin: 6px 0; +} + +/* Box delle istruzioni */ +.instructions-box { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: white; + text-shadow: 2px 2px 4px rgba(0,0,0,0.8); + font-family: 'Press Start 2P', cursive; +} + +.instructions-box h3 { + margin-bottom: 10px; + font-size: 1.4rem; + text-transform: uppercase; + text-align: center; +} + +.instructions-box p { + margin: 6px 0; + font-size: 1.1rem; + text-align: center; +} + + + + /* Animazione per "Clicca per giocare" */ .pulse-text { font-size: 1.5rem; diff --git a/index.php b/index.php index 21ed62c..2b68b9f 100644 --- a/index.php +++ b/index.php @@ -12,5 +12,5 @@ http_response_code(404); $p = 'error'; } - include $__DIR__ . '/pages/' . $route[$p]; + include __DIR__ . '/pages/' . $route[$p]; ?> \ No newline at end of file diff --git a/js/fase1.js b/js/fase1.js index 139fdd3..dec36f0 100755 --- a/js/fase1.js +++ b/js/fase1.js @@ -2,9 +2,33 @@ import * as THREE from 'three'; import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; -// 1. SCENA E CAMERA +// 1a. INIZIALIZZAZIONE CARICAMENTO ASSETS +const manager = new THREE.LoadingManager(); +manager.onStart = function (url, itemsLoaded, itemsTotal) { + document.getElementById("loading-text").innerHTML = `Inizio caricamento: ${url}`; + console.log(`Inizio caricamento: ${url}`); +}; +manager.onLoad = function () { + document.getElementById('loading-screen').style.display = 'none'; + animate(); // avvia il rendering +}; +manager.onProgress = function (url, itemsLoaded, itemsTotal) { + // Calcola la percentuale + const percent = (itemsLoaded / itemsTotal) * 100; + document.getElementById('loading-bar').style.width = percent + '%'; + + // Aggiorna il testo + document.getElementById("loading-text").innerHTML = `Caricamento: ${itemsLoaded} di ${itemsTotal} - ${url}`; + console.log(`Caricamento: ${itemsLoaded} di ${itemsTotal} - ${url}`); +}; +manager.onError = function (url) { + document.getElementById("loading-text").innerHTML = `Errore nel caricamento: ${url}`; + console.log(`Errore nel caricamento: ${url}`); +}; + +// 1b. SCENA E CAMERA const scene = new THREE.Scene(); -scene.background = new THREE.Color(0x87ceeb); +scene.background = new THREE.Color(0x33ccff); // Cielo più azzurro const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); @@ -12,14 +36,115 @@ document.body.appendChild(renderer.domElement); window.addEventListener('resize', function () { renderer.setSize(window.innerWidth, window.innerHeight) }) +// AGGIUNTA MARE +const oceanGeometry = new THREE.PlaneGeometry(2000, 2000); +const oceanMaterial = new THREE.MeshStandardMaterial({ + color: 0x0077be, + transparent: true, + opacity: 0.8 +}); +const ocean = new THREE.Mesh(oceanGeometry, oceanMaterial); +ocean.rotation.x = -Math.PI / 2; +ocean.position.y = -0.2; // Leggermente sotto l'isola +scene.add(ocean); // 2. LUCI E AMBIENTE -scene.add(new THREE.AmbientLight(0xffffff, 1)); -const island = new THREE.Mesh(new THREE.CircleGeometry(20, 32), new THREE.MeshStandardMaterial({ color: 0x4df556 })); // #4df555 -island.rotation.x = -Math.PI / 2; -scene.add(island); +scene.add(new THREE.AmbientLight(0xffffff, 0.8)); +const sunLight = new THREE.DirectionalLight(0xffffff, 1); +sunLight.position.set(10, 20, 10); +scene.add(sunLight); + +const islandGroup = new THREE.Group(); + +const altezzaIsola = 0.5; // Quanto sta il prato sopra il livello del mare (0.0) +const altezzaOcchi = 1.6; // L'altezza standard della telecamera + +// --- IL FONDALE (Sabbia scura sul fondo) --- +const floorGeo = new THREE.PlaneGeometry(1000, 1000); +const floorMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a }); // Grigio scuro/Fondale +const floor = new THREE.Mesh(floorGeo, floorMat); +floor.rotation.x = -Math.PI / 2; +floor.position.y = -3; // 5 metri sotto il livello del mare +scene.add(floor); + +// --- IL PRATO --- +const raggioPrato = 19; +const grassGeo = new THREE.CircleGeometry(raggioPrato, 64); +const grassMat = new THREE.MeshStandardMaterial({ color: 0x4df556 }); +const grass = new THREE.Mesh(grassGeo, grassMat); +grass.rotation.x = -Math.PI / 2; + +// Posizioniamo il prato all'altezza desiderata +grass.position.y = altezzaIsola; +islandGroup.add(grass); + +// --- LA SABBIA (Cilindro Svasato) --- +const altezzaSabbia = 1.5; // Altezza totale del blocco sabbia +const sandGeo = new THREE.CylinderGeometry(raggioPrato, 22 + 10, altezzaSabbia + 3, 64, 1, true); +const sandMat = new THREE.MeshStandardMaterial({ color: 0xedc9af, side: THREE.DoubleSide }); +const sand = new THREE.Mesh(sandGeo, sandMat); + +// Per far combaciare la cima del cilindro con il prato: +// L'altezza è 1.5, il centro del cilindro deve stare a (AltezzaIsola - metà altezza cilindro) +sand.position.y = altezzaIsola - (altezzaSabbia / 2) - 1.5; +islandGroup.add(sand); + +scene.add(islandGroup); +scene.fog = new THREE.Fog(0x33ccff, 20, 100); // 3. CONTROLLI +window.totalTime; +class PausableTimer { + constructor(callback, delay) { + this.callback = callback; + this.delay = delay; // salvi il delay originale + this.remaining = delay; + this.timerId = null; + this.start = null; + this.loop = null; + this.cont = 0; + window.totalTime = delay / 1000; + } + pause() { + if (this.timerId) { + clearTimeout(this.timerId); + this.timerId = null; + // Calcola il tempo passato + this.remaining -= Date.now() - this.start; + } + if (this.loop) { + clearInterval(this.loop); + this.loop = null; + } + console.log("pausa"); + } + resume() { + console.log("resume"); + if (this.timerId) return; // già in esecuzione + if (this.remaining > 0) { + this.start = Date.now(); + this.timerId = setTimeout(() => { + this.pause(); + this.callback(); + }, this.remaining); + + // Se il ciclo di aggiornamento non è già attivo, avvialo + if (!this.loop) { + this.loop = setInterval(() => { + this.cont++; + // Aggiorna il timer visualizzato + document.getElementById("minuti").innerHTML = String(Math.floor((window.totalTime - this.cont) / 60)).padStart(2, '0'); + document.getElementById("secondi").innerHTML = String((window.totalTime - this.cont) % 60).padStart(2, '0'); + console.log(`${document.getElementById("minuti").innerHTML}:${document.getElementById("secondi").innerHTML}`); + }, 1000); + } + } + } +} +const timer = new PausableTimer(() => { + localStorage.setItem("punteggioFase1", document.getElementById("score").innerHTML.trim()); + window.location = "?pagina=mid"; +}, 60000) function lock() { document.getElementById('start').style.display = 'none'; document.getElementById('punti').style.display = 'block'; @@ -27,22 +152,27 @@ function lock() { document.getElementById('crosshair').style.display = 'block'; } function unlock() { - const startDiv = document.getElementById('start'); - startDiv.style.display = 'flex'; // Forza il layout Flexbox - // Nascondi l'HUD + timer.pause(); + document.getElementById('start').style.display = 'flex'; document.getElementById('punti').style.display = 'none'; document.getElementById('tempo').style.display = 'none'; document.getElementById('crosshair').style.display = 'none'; } const controls = new PointerLockControls(camera, document.body); -document.getElementById('start').addEventListener('click', () => controls.lock()); -controls.addEventListener('lock', lock); -controls.addEventListener('unlock', unlock); +document.getElementById('start').addEventListener('click', () => { + controls.lock(); + lock(); +}); +controls.addEventListener('lock', () => { + lock(); + timer.resume(); +}); +controls.addEventListener('unlock', () => { unlock(); }); -// 4. GESTIONE RIFIUTI (20 pezzi) +// 4. GESTIONE OGGETTI DI SCENA (20 rifiuti + 6 alberi) const trashArray = []; let score = 0; -const loader = new GLTFLoader(); +const loader = new GLTFLoader(manager); loader.load('assets/models/rifiuto.glb', (gltf) => { const mesh = gltf.scene.getObjectByName("Trash_Pile_03_GEO"); @@ -51,41 +181,151 @@ loader.load('assets/models/rifiuto.glb', (gltf) => { const clone = mesh.clone(); scene.add(clone); trashArray.push(clone); - spawn(clone); + spawn(clone, 0.2); } }); +loader.load('assets/models/tree.glb', (gltf) => { + const mesh = gltf.scene; + mesh.scale.set(4, 4, 4); + console.log(mesh); + + function spawn(obj, radius, minDistance) { + const points = []; + const attempts = 1000; // Numero massimo di tentativi per trovare un punto valido + for (let i = 0; i < attempts; i++) { + let angle = Math.random() * 2 * Math.PI; + let dist = Math.random() * radius; + let x = dist * Math.cos(angle); + let z = dist * Math.sin(angle); + let valid = true; + for (let j = 0; j < points.length; j++) { + const dx = x - points[j].x; + const dz = z - points[j].z; + const distance = Math.sqrt(dx * dx + dz * dz); + if (distance < minDistance) { + valid = false; + break; + } + } + if (valid) { + points.push({ x: x, z: z }); + } + } + + // Spawna gli alberi nei punti generati + for (let i = 0; i < points.length; i++) { + const clone = obj.clone(); + const a = Math.random() * Math.PI * 2; + const r = Math.random() * 18; + const x = Math.cos(a) * r; + const z = Math.sin(a) * r; + // Y deve essere altezzaIsola + un piccolo offset + clone.position.set(points[i].x, altezzaIsola - 0.1, points[i].z); + clone.rotation.y = Math.random() * Math.PI * 2; // Rotazione tra 0 e 360 gradi + scene.add(clone); + } + } + spawn(mesh, 18, 8); // Raggio 18, distanza minima 5 +}); +console.log(scene) + function spawn(obj) { const a = Math.random() * Math.PI * 2; const r = Math.random() * 18; - obj.position.set(Math.cos(a) * r, 0.3, Math.sin(a) * r); + const x = Math.cos(a) * r; + const z = Math.sin(a) * r; + // Y deve essere altezzaIsola + un piccolo offset + obj.position.set(x, altezzaIsola + 0.1, z); } // 5. MOVIMENTO E COLLISIONI +const arrow = false; const keys = {}; document.onkeydown = (e) => keys[e.code] = true; document.onkeyup = (e) => keys[e.code] = false; +// Setup Raycaster (fuori dal loop animate) +const raycaster = new THREE.Raycaster(); +const downVector = new THREE.Vector3(0, -1, 0); + +// --- CONFIGURAZIONE MOVIMENTO --- +let canLeaveIsland = false; // Se false, l'acqua blocca il movimento. +let lastSafePosition = camera.position.clone(); + function animate() { requestAnimationFrame(animate); - if (controls.isLocked) { - if (keys['KeyW']) controls.moveForward(0.15); - if (keys['KeyS']) controls.moveForward(-0.15); - if (keys['KeyA']) controls.moveRight(-0.15); - if (keys['KeyD']) controls.moveRight(0.15); - camera.position.y = 1.6; - // Controllo collisioni - const pPos = camera.position.clone(); - pPos.y -= 1.0; + if (controls.isLocked) { + // 1. Salva la posizione attuale prima del movimento + const oldPos = camera.position.clone(); + + // 2. Esegui il movimento WASD + if (arrow) { + if (keys['ArrowUp']) controls.moveForward(0.15); + if (keys['ArrowDown']) controls.moveForward(-0.15); + if (keys['ArrowLeft']) controls.moveRight(-0.15); + if (keys['ArrowRight']) controls.moveRight(0.15); + } else { + if (keys['KeyW']) controls.moveForward(0.15); + if (keys['KeyS']) controls.moveForward(-0.15); + if (keys['KeyA']) controls.moveRight(-0.15); + if (keys['KeyD']) controls.moveRight(0.15); + } + + // 3. --- GESTIONE ALTEZZA DINAMICA --- + const rayOrigin = camera.position.clone(); + rayOrigin.y = 10; // Spara dall'alto verso il basso + raycaster.set(rayOrigin, downVector); + + // Controlliamo Prato e Sabbia + const intersects = raycaster.intersectObjects([grass, sand]); + + if (intersects.length > 0) { + // Logica del "Punto più alto": + // Se il raggio colpisce sia prato che sabbia, prendiamo il prato. + let highestPoint = -Infinity; + for (let i = 0; i < intersects.length; i++) { + if (intersects[i].point.y > highestPoint) { + highestPoint = intersects[i].point.y; + } + } + + // Applichiamo l'altezza alla camera + camera.position.y = highestPoint + altezzaOcchi; + + // --- CONTROLLO BARRIERA ACQUA --- + // Se il punto più alto è sotto il livello del mare (0) + // e non possiamo uscire, blocchiamo il movimento + if (!canLeaveIsland && highestPoint < 0) { + camera.position.x = oldPos.x; + camera.position.z = oldPos.z; + camera.position.y = oldPos.y; + } else { + // Posizione valida, la salviamo + lastSafePosition.copy(camera.position); + camera.position.y = camera.position.y > (-2.25 + altezzaOcchi) ? camera.position.y : (-2.25 + altezzaOcchi); + } + } else { + // --- FUORI DALL'ISOLA (VUOTO) --- + if (!canLeaveIsland) { + camera.position.copy(oldPos); + } else { + camera.position.y = -2.25 + altezzaOcchi; // Volo sull'acqua + } + } + + // --- COLLISIONI RIFIUTI --- trashArray.forEach(t => { - if (new THREE.Box3().setFromObject(t).expandByScalar(0.3).containsPoint(pPos)) { + if (camera.position.distanceTo(t.position) < 1.8) { score++; document.getElementById('score').innerText = score; spawn(t); } }); } + renderer.render(scene, camera); } -animate(); \ No newline at end of file +document.getElementById("minuti").innerHTML = String(Math.floor(window.totalTime / 60)).padStart(2, '0'); +document.getElementById("secondi").innerHTML = String(window.totalTime % 60).padStart(2, '0'); \ No newline at end of file diff --git a/materiale/STRUTTURA.md b/materiale/STRUTTURA.md index c7b11fc..e7aec94 100644 --- a/materiale/STRUTTURA.md +++ b/materiale/STRUTTURA.md @@ -75,6 +75,9 @@ Workflow del gioco: ``` Inizio -> 60s per raccogliere i rifiuti -> Salvataggio del punteggio in localStorage -> Schermata intermedia -> *5s per separare i rifiuti -> Invio dati della partita al database (nome squadra, rifiuti raccolti nella fase 1, rifiuti separati correttamente nella fase 2, punteggio finale, data della partita) -> caricamento schermata finale con classifica ridotta (posizione, squadra, punteggio) e pulsanti per giocare ancora, impostazioni e la classifica completa (classifica.php) con tutti i dati e tutte le partite giocate (classifica ridotta: solo le prime 5/10 partite) ``` +``` +start -> mid -> sep -> end +``` Indirizzamento pagine in index.php ``` diff --git a/pages/fase1.html b/pages/fase1.html index 8121ba2..92ee42a 100755 --- a/pages/fase1.html +++ b/pages/fase1.html @@ -6,32 +6,54 @@ + +
+
+
+
+
Loading...
+
+ +
- titolo + titolo

Controlli

-

WASD Movimento

-

Mouse Camera

+

Movimento

+

Freccia su / W Avanti

+

Freccia giù / S Indietro

+

Freccia destra / D Destra

+

Freccia sinistra / A Sinistra

+

Mouse Punto di vista

Clicca per giocare
-
+
+
+

Istruzioni

+

Obiettivo: raccogliere tutti i rifiuti sull’isola

+

Attenzione allo scadere del tempo!

+

Usa i comandi per esplorare e ripulire

+
+
+ +
Rifiuti: 0
-
Tempo: XX:XX
+
Tempo: XX:XX
+