commit 3722e1fd4982f4ade50bb0d60b7397d011ba0137 Author: SimonezYT Date: Wed Apr 1 11:26:57 2026 +0200 inizio classifica diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..839beb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# ---> macOS +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# ---> Windows +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# ---> Dreamweaver +# DW Dreamweaver added files +_notes +_compareTemp +configs/ +dwsync.xml +dw_php_codehinting.config +*.mno + +# ---> Android +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1522da2 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# 🌍 Save the Island - Progetto per la Giornata della Terra + +**Save the Island** è un videogioco web in 3D nato per sensibilizzare gli utenti sulla gestione dei rifiuti e l'importanza del riciclo. Il progetto adotta un'architettura Fullstack basata su **XAMPP** per la gestione di una classifica globale e il salvataggio dei record. + +--- + +## 🛠️ Architettura Tecnica +* **Entrypoint:** `index.php` (Pagina di atterraggio con trailer e controlli). +* **Backend:** PHP + MySQL (XAMPP) per la gestione del database e dei punteggi. +* **Frontend:** **Three.js** (motore 3D), JavaScript (ES6+), CSS3. +* **Trasferimento Dati:** Utilizzo di Cookie temporanei o `localStorage` per mantenere i dati tra le diverse fasi di gioco. + +--- + +## 🗺️ Roadmap di Sviluppo + +### 🚩 Milestone Generali (Nucleo del Progetto) +- [⚠️] **Design UI & Mockup:** Finalizzazione degli asset grafici (basati sullo schema Draw.io). +- [❌] **Configurazione Database:** Creazione delle tabelle `classifica` e `records` su MySQL. +- [❌] **Logica Entrypoint:** Sviluppo di `index.php` con video di sfondo e overlay dei comandi. +- [⚠️] **Sistema di Trasferimento:** Implementazione logica per il passaggio dei dati dalla Fase 1 alla Fase 2. +- [❌] **Gestione Impostazioni:** Pannello per regolare il volume e inserire il nome della squadra (servira' nella classifica). +- [❌] **Pagina Dinamica:** Creazione della classifica in PHP con recupero dati in tempo reale. + +### 🏝️ Fase 1: La Raccolta (Timer: 1m) +- [✅] **Ambiente di gioco:** Modellazione dell'isola 3D con Three.js e gestione dei confini della mappa. +- [✅] **Sistema di Spawn:** Posizionamento casuale dei rifiuti su 20 coordinate casuali. +- [✅] **Sistema Ostacoli:** Inserimento di modelli 3D di alberi e oggetti ambientali decorativi. +- [✅] **Meccaniche di Raccolta:** Gestione delle collisioni e incremento del punteggio ecologico. +- [✅] **Interfaccia (HUD):** Overlay con Timer (60s), contatore rifiuti e disattivazione della pausa. +- [⚠️] **Schermata di caricamento:** Schermata di caricamento iniziale +- [⚠️] **Schermata iniziale:** Schermata iniziale +- [❌] **Rifinitura (Polish):** Animazioni di raccolta e ottimizzazione delle mesh 3D. +- [✅] **Timer:** Meccanica del timer per il tempo di raccolta dei rifiuti +- [❌] **Personaggio:** Cilindro che rappresenta il personaggio (fatto per collisioni piu' precise) + +### ♻️ Fase 2: Lo Smistamento (Timer: 5s * Punteggio) +- [❌] **Timer Dinamico:** Calcolo del tempo a disposizione basato sul successo della Fase 1. +- [❌] **Motore di Smistamento:** Logica di convalida (Rifiuto ↔️ Bidone corretto). +- [❌] **Interfaccia Utente:** Layout con i 6 bidoni (Plastica, Umido, Indifferenziata, Vetro, Carta, Alluminio). +- [❌] **Gestione Input:** Meccanica di interazione tramite Drag & Drop o selezione rapida. +- [❌] **Feedback Visivo:** Effetti sonori e visivi per risposte corrette o errate. + +### 🏆 Fase Finale: Classifica & Record +- [❌] **Calcolo Punteggio:** Elaborazione dei risultati finali e calcolo dei bonus velocità. +- [❌] **Verifica Record:** Confronto del punteggio con il record personale salvato. +- [❌] **Classifica Globale:** Script PHP per l'invio e la visualizzazione della Top 10 dal database. +- [❌] **Schermata Game Over:** Riepilogo statistiche e opzione per riavviare la partita. + +--- + +## 📂 Struttura delle Cartelle +* `/assets` - Modelli 3D, video loop, texture dei rifiuti e icone dei bidoni. +* `/css` - Fogli di stile per i menu e l'interfaccia di gioco (HUD). +* `/js` - Logica core e rendering (Three.js, stage1.js, stage2.js). +* `/php` - Script per il backend (`db_connect.php`, `save_score.php`). +* `/games` - Pagine HTML dedicate alle sessioni di gioco (`fase1.html`, `fase2.html`). +* `index.php` - Homepage e controller principale del progetto. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100755 index 0000000..516e2e6 --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + Semplice Island FPS + + + + +
+
+
+
+
Loading...
+
+ + +
+ +
+
+
+ titolo +
+
+
+
+

Controlli

+

WASD Movimento

+

Mouse Camera

+
+
+
+ Clicca per giocare +
+
+
+
+
+ + +
Rifiuti: 0
+
Tempo: XX:XX
+
+
+ + + + \ No newline at end of file diff --git a/models/rifiuto.glb b/models/rifiuto.glb new file mode 100755 index 0000000..410584e Binary files /dev/null and b/models/rifiuto.glb differ diff --git a/models/tree.glb b/models/tree.glb new file mode 100644 index 0000000..37f361d Binary files /dev/null and b/models/tree.glb differ diff --git a/progetto/TODO.txt b/progetto/TODO.txt new file mode 100755 index 0000000..12e65a1 --- /dev/null +++ b/progetto/TODO.txt @@ -0,0 +1 @@ +TODO: \ No newline at end of file diff --git a/progetto/idea gioco.drawio b/progetto/idea gioco.drawio new file mode 100755 index 0000000..09a02d2 --- /dev/null +++ b/progetto/idea gioco.drawio @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/progetto/idea gioco.png b/progetto/idea gioco.png new file mode 100755 index 0000000..f3f1df4 Binary files /dev/null and b/progetto/idea gioco.png differ diff --git a/progetto/progetto.txt b/progetto/progetto.txt new file mode 100755 index 0000000..df46cb9 --- /dev/null +++ b/progetto/progetto.txt @@ -0,0 +1,8 @@ +Nome gioco: Save the Island + +posizioni: 20 posizioni predefinite scelte random +tempo 1° fase: 1m +tempo 2° fase: 5s * punteggio +schermata iniziale: click + controlli + bckg trailer (loop video) + titolo +schermata intermedia: istruzioni +schermata finale: punteggio finale + record + classifica(?) \ No newline at end of file diff --git a/progetto/schermata iniziale.png b/progetto/schermata iniziale.png new file mode 100755 index 0000000..fc24ac4 Binary files /dev/null and b/progetto/schermata iniziale.png differ diff --git a/punteggio.html b/punteggio.html new file mode 100644 index 0000000..8055c87 --- /dev/null +++ b/punteggio.html @@ -0,0 +1,14 @@ + + + + + + Test Punti + + +

Punti: 0

+ + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100755 index 0000000..65e4596 --- /dev/null +++ b/script.js @@ -0,0 +1,331 @@ +import * as THREE from 'three'; +import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; +import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; + +// 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(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); +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, 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("punteggio", document.getElementById("score").innerHTML.trim()); + window.location = "punteggio.html"; +}, 60000) +function lock() { + document.getElementById('start').style.display = 'none'; + document.getElementById('punti').style.display = 'block'; + document.getElementById('tempo').style.display = 'block'; + document.getElementById('crosshair').style.display = 'block'; +} +function unlock() { + 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(); + lock(); +}); +controls.addEventListener('lock', () => { + lock(); + timer.resume(); +}); +controls.addEventListener('unlock', () => { unlock(); }); + +// 4. GESTIONE OGGETTI DI SCENA (20 rifiuti + 6 alberi) +const trashArray = []; +let score = 0; +const loader = new GLTFLoader(manager); + +loader.load('models/rifiuto.glb', (gltf) => { + const mesh = gltf.scene.getObjectByName("Trash_Pile_03_GEO"); + mesh.geometry.center(); // Centra l'oggetto per collisioni precise + for (let i = 0; i < 20; i++) { + const clone = mesh.clone(); + scene.add(clone); + trashArray.push(clone); + spawn(clone, 0.2); + } +}); + +loader.load('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; + 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) { + // 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 (camera.position.distanceTo(t.position) < 1.8) { + score++; + document.getElementById('score').innerText = score; + spawn(t); + } + }); + } + + renderer.render(scene, camera); +} +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/style.css b/style.css new file mode 100755 index 0000000..709990e --- /dev/null +++ b/style.css @@ -0,0 +1,178 @@ +:root { + --top: 10px; + --border: 20px; + --text: 1.5rem; +} +body { + margin: 0; + overflow: hidden; + font-family: sans-serif; +} +#punti { + position: absolute; + top: var(--top); + left: var(--border); + color: white; + font-size: var(--text); + pointer-events: none; + display: none; +} +#tempo { + position: absolute; + top: var(--top); + right: var(--border); + color: white; + font-size: var(--text); + pointer-events: none; + display: none; +} +#punti, #tempo { + position: absolute; + top: var(--top); + color: white; + font-size: var(--text); + pointer-events: none; + display: none; + /* Aggiungi questa riga */ + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.7); + font-weight: bold; +} + +/* Cursore */ +#crosshair { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 24px; + font-family: sans-serif; + pointer-events: none; + display: none; /* Sarà visualizzato solo durante il gioco */ + z-index: 10; +} + +/* Schermata iniziale del gioco */ +#start { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + color: white; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + /* Queste 4 righe garantiscono la centratura totale */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 100; /* Assicurati che sia sopra al renderer di Three.js */ +} + +/* Video a tutto schermo */ +#bg-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + object-fit: cover; /* Riempie lo schermo senza deformare */ +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: 0; + pointer-events: none; /* Il click attraversa l'overlay e arriva a #start */ +} + +.content { + position: relative; + z-index: 1; + height: 100%; + width: 100%; /* Aggiunto */ + display: flex; + flex-direction: column; +} + +/* Divisione 50% Sopra */ +.top-section { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.main-title { + max-width: 80%; + height: auto; +} + +/* Divisione 50% Sotto */ +.bottom-section { + flex: 1; + display: flex; + flex-direction: row; + width: 100%; /* Forza l'espansione orizzontale */ + align-items: flex-start; + padding-top: 20px; +} + +.item { + /* flex: 1 0 33%; significa: cresci, non restringerti, base 33% */ + flex: 1 0 33.33%; + display: flex; + justify-content: center; + text-align: center; +} + +/* Styling dei Controlli (KBD) */ +.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; +} + +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; +} + +/* Animazione per "Clicca per giocare" */ +.pulse-text { + font-size: 1.5rem; + text-transform: uppercase; + letter-spacing: 2px; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.4; } + 100% { opacity: 1; } +} \ No newline at end of file diff --git a/textures/rifiuto/Trash_AlbedoTransparency.png b/textures/rifiuto/Trash_AlbedoTransparency.png new file mode 100755 index 0000000..2ca6634 Binary files /dev/null and b/textures/rifiuto/Trash_AlbedoTransparency.png differ diff --git a/textures/rifiuto/Trash_Ambient_Occlusion.png b/textures/rifiuto/Trash_Ambient_Occlusion.png new file mode 100755 index 0000000..2ca6634 Binary files /dev/null and b/textures/rifiuto/Trash_Ambient_Occlusion.png differ diff --git a/textures/rifiuto/Trash_MetallicSmoothness.png b/textures/rifiuto/Trash_MetallicSmoothness.png new file mode 100755 index 0000000..7fd18e3 Binary files /dev/null and b/textures/rifiuto/Trash_MetallicSmoothness.png differ diff --git a/textures/rifiuto/Trash_Normal.png b/textures/rifiuto/Trash_Normal.png new file mode 100755 index 0000000..1d6da08 Binary files /dev/null and b/textures/rifiuto/Trash_Normal.png differ diff --git a/textures/rifiuto/Trash_Roughness.png b/textures/rifiuto/Trash_Roughness.png new file mode 100755 index 0000000..74acbac Binary files /dev/null and b/textures/rifiuto/Trash_Roughness.png differ