🎮ArcadeLab

🚌 Estacionar Ônibus 3D Real

by DriftPenguin96
273 lines12.7 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>🚌 Estacionar Ônibus 3D Real</title>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/js/loaders/GLTFLoader.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.158.0/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/cannon-es@0.20.0/dist/cannon-es.min.js"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; font-family: Arial, sans-serif; }
        body { background: #f0f4f8; overflow: hidden; }
        canvas { display: block; }
        .ui { position: absolute; top: 15px; left: 15px; z-index: 10; background: rgba(255,255,255,0.92); padding: 12px 16px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.2); }
        .controles { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: grid; grid-template-columns: repeat(3, 85px); gap: 10px; z-index: 10; }
        button { padding: 12px; font-size: 22px; border: none; border-radius: 8px; background: #0066cc; color: white; box-shadow: 0 2px 5px rgba(0,0,0,0.3); touch-action: manipulation; }
        button:active { background: #004c99; transform: scale(0.97); }
        .mensagem { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255,255,255,0.96); padding: 25px 35px; border-radius: 12px; font-size: 28px; font-weight: bold; display: none; z-index: 20; box-shadow: 0 5px 20px rgba(0,0,0,0.3); }
    </style>
</head>
<body>
    <div class="ui">
        <h3>Fase: <span id="fase">1</span></h3>
        <p id="descricao">Vaga reta ampla</p>
    </div>
    <div class="mensagem" id="msg"></div>
    <div class="controles">
        <div></div>
        <button onclick="mover('frente')">↑</button>
        <div></div>
        <button onclick="virar('esq')">←</button>
        <button onclick="reiniciar()">🔄</button>
        <button onclick="virar('dir')">→</button>
        <div></div>
        <button onclick="mover('tras')">↓</button>
        <div></div>
    </div>

    <script>
        let cena, camera, renderizador, controles, mundoFisica;
        let onibus = { modelo: null, corpo: null };
        let faseAtual = 0;
        const velocidade = 1.8;
        const rotacao = 0.035;

        // ✅ Modelo GLB de ônibus real gratuito e hospedado
        const modeloOnibusURL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Bus/glTF/Bus.glb";[[__LINK_ICON]](https://threejs.org/manual/zh/loading-3d-models.html?f_link_type=f_linkinlinenote&flow_extra=eyJpbmxpbmVfZGlzcGxheV9wb3NpdGlvbiI6MCwiZG9jX3Bvc2l0aW9uIjowLCJkb2NfaWQiOiIwNGI1MDViODFiYTZjY2U2LWY0YWVlODQ4OTZlMjA0ZjMifQ%3D%3D "[__LINK_ICON]")

        // 🎯 Fases progressivas
        const fases = [
            {
                desc: "Fase 1 — Vaga reta ampla",
                vaga: { pos: [0, 0, -22], ang: 0, tolDist: 2.2, tolAng: 0.22 },
                obstaculos: []
            },
            {
                desc: "Fase 2 — Vaga levemente inclinada",
                vaga: { pos: [0.4, 0, -23], ang: Math.PI/12, tolDist: 2.0, tolAng: 0.20 },
                obstaculos: []
            },
            {
                desc: "Fase 3 — Vaga com poste lateral",
                vaga: { pos: [0, 0, -22], ang: 0, tolDist: 1.9, tolAng: 0.19 },
                obstaculos: [{ tipo: "cilindro", pos: [-2.8, 0, -21], raio: 0.35, alt: 2.2 }]
            },
            {
                desc: "Fase 4 — Vaga estreita inclinada",
                vaga: { pos: [0.6, 0, -24], ang: -Math.PI/10, tolDist: 1.7, tolAng: 0.17 },
                obstaculos: [{ tipo: "caixa", pos: [-2.2, 0, -20], dim: [1.1, 1.6, 0.8] }]
            },
            {
                desc: "Fase 5 — Desafio final apertado",
                vaga: { pos: [0.2, 0, -23], ang: Math.PI/18, tolDist: 1.5, tolAng: 0.15 },
                obstaculos: [
                    { tipo: "cilindro", pos: [-2.6, 0, -22], raio: 0.35, alt: 2.2 },
                    { tipo: "caixa", pos: [2.4, 0, -21], dim: [1.2, 1.8, 0.9] }
                ]
            }
        ];

        function iniciarJogo() {
            cena = new THREE.Scene();
            cena.background = new THREE.Color(0x87ceeb);
            camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 120);
            camera.position.set(6, 7, 14);
            camera.lookAt(0, 1, 0);

            renderizador = new THREE.WebGLRenderer({ antialias: true });
            renderizador.setSize(window.innerWidth, window.innerHeight);
            renderizador.shadowMap.enabled = true;
            document.body.appendChild(renderizador.domElement);

            controles = new THREE.OrbitControls(camera, renderizador.domElement);
            controles.target.set(0, 1, 0);
            controles.minDistance = 6;
            controles.maxDistance = 22;

            adicionarIluminacao();
            criarChaoEVaga();
            iniciarFisica();
            carregarOnibusReal();
            iniciarFase();
            animar();
        }

        function adicionarIluminacao() {
            const luzAmb = new THREE.AmbientLight(0xffffff, 0.7);
            cena.add(luzAmb);
            const luzDir = new THREE.DirectionalLight(0xffffff, 0.9);
            luzDir.position.set(8, 15, 6);
            luzDir.castShadow = true;
            luzDir.shadow.mapSize.set(2048, 2048);
            cena.add(luzDir);
        }

        function criarChaoEVaga() {
            const chaoGeo = new THREE.PlaneGeometry(60, 60);
            const chaoMat = new THREE.ShadowMaterial({ opacity: 0.4 });
            const chao = new THREE.Mesh(chaoGeo, chaoMat);
            chao.rotation.x = -Math.PI / 2;
            chao.receiveShadow = true;
            cena.add(chao);

            const vagaGeo = new THREE.PlaneGeometry(9, 2.6);
            const vagaMat = new THREE.MeshStandardMaterial({ color: 0xcfe2f3, transparent: true, opacity: 0.7 });
            cena.userData.vagaMesh = new THREE.Mesh(vagaGeo, vagaMat);
            cena.userData.vagaMesh.receiveShadow = true;
            cena.add(cena.userData.vagaMesh);
        }

        function iniciarFisica() {
            mundoFisica = new CANNON.World();
            mundoFisica.gravity.set(0, -9.82, 0);
            mundoFisica.broadphase = new CANNON.NaiveBroadphase();
        }

        function carregarOnibusReal() {
            const carregador = new THREE.GLTFLoader();[[__LINK_ICON]](https://threejs.org/manual/zh/loading-3d-models.html?f_link_type=f_linkinlinenote&flow_extra=eyJpbmxpbmVfZGlzcGxheV9wb3NpdGlvbiI6MCwiZG9jX3Bvc2l0aW9uIjowLCJkb2NfaWQiOiIwNGI1MDViODFiYTZjY2U2LWY0YWVlODQ4OTZlMjA0ZjMifQ%3D%3D "[__LINK_ICON]")
            carregador.load(modeloOnibusURL, (gltf) => {
                onibus.modelo = gltf.scene;
                onibus.modelo.scale.set(1.7, 1.7, 1.7);
                onibus.modelo.position.set(0, 0.25, 6);
                onibus.modelo.rotation.y = Math.PI;
                onibus.modelo.traverse(obj => { if (obj.isMesh) { obj.castShadow = true; obj.receiveShadow = true; } });
                cena.add(onibus.modelo);

                const formaOnibus = new CANNON.Box(new CANNON.Vec3(3.7, 1.6, 9.2));
                onibus.corpo = new CANNON.Body({ mass: 280, shape: formaOnibus, position: new CANNON.Vec3(0, 0.3, 6) });
                onibus.corpo.linearDamping = 0.94;
                onibus.corpo.angularDamping = 0.94;
                mundoFisica.addBody(onibus.corpo);
            }, undefined, (erro) => console.error("Erro ao carregar ônibus:", erro));
        }

        function iniciarFase() {
            const dados = fases[faseAtual];
            document.getElementById("fase").textContent = faseAtual + 1;
            document.getElementById("descricao").textContent = dados.desc;

            cena.userData.vagaMesh.position.set(dados.vaga.pos[0], 0.02, dados.vaga.pos[2]);
            cena.userData.vagaMesh.rotation.y = dados.vaga.ang;

            cena.userData.obstaculos = [];
            dados.obstaculos.forEach(def => {
                let malha, corpo;
                if (def.tipo === "cilindro") {
                    const geo = new THREE.CylinderGeometry(def.raio, def.raio, def.alt, 24);
                    const mat = new THREE.MeshStandardMaterial({ color: 0x555555 });
                    malha = new THREE.Mesh(geo, mat);
                    malha.position.set(def.pos[0], def.alt/2, def.pos[2]);
                    malha.castShadow = true; malha.receiveShadow = true;
                    corpo = new CANNON.Body({ mass: 0, shape: new CANNON.Cylinder(def.raio, def.raio, def.alt, 24) });
                    corpo.position.set(def.pos[0], def.alt/2, def.pos[2]);
                } else {
                    const geo = new THREE.BoxGeometry(...def.dim);
                    const mat = new THREE.MeshStandardMaterial({ color: 0x8b5a2b });
                    malha = new THREE.Mesh(geo, mat);
                    malha.position.set(def.pos[0], def.dim[1]/2, def.pos[2]);
                    malha.castShadow = true; malha.receiveShadow = true;
                    corpo = new CANNON.Body({ mass: 0, shape: new CANNON.Box(new CANNON.Vec3(...def.dim.map(v => v/2))) });
                    corpo.position.set(def.pos[0], def.dim[1]/2, def.pos[2]);
                }
                cena.add(malha);
                mundoFisica.addBody(corpo);
                cena.userData.obstaculos.push({ malha, corpo });
            });
        }

        function mover(direcao) {
            if (!onibus.corpo) return;
            const sinal = direcao === "frente" ? -1 : 1;
            const dir = new THREE.Vector3(0, 0, sinal).applyQuaternion(onibus.modelo.quaternion);
            onibus.corpo.velocity.set(dir.x * velocidade, 0, dir.z * velocidade);
        }

        function virar(lado) {
            if (!onibus.corpo) return;
            const rot = lado === "dir" ? -rotacao : rotacao;
            onibus.corpo.angularVelocity.y = rot;
        }

        function verificarEstacionamento() {
            if (!onibus.modelo) return;
            const dados = fases[faseAtual];
            const centro = onibus.modelo.position;
            const angulo = onibus.modelo.rotation.y;
            const dist = Math.sqrt(
                Math.pow(centro.x - dados.vaga.pos[0], 2) +
                Math.pow(centro.z - dados.vaga.pos[2], 2)
            );
            const difAng = Math.abs(THREE.MathUtils.euclideanModulo(angulo - dados.vaga.ang, Math.PI * 2));
            const bateu = onibus.corpo.collisionResponse === "collide";

            if (bateu) {
                mostrarMensagem("⚠️ Cuidado! Bateu", "#dc2626");
                return;
            }
            if (dist < dados.vaga.tolDist && difAng < dados.vaga.tolAng) {
                mostrarMensagem("✅ Estacionado! Próxima fase", "#16a34a");
                setTimeout(() => {
                    if (faseAtual < fases.length - 1) {
                        faseAtual++;
                        reiniciar();
                    } else {
                        mostrarMensagem("🏆 Parabéns! Todas concluídas", "#2563eb");
                    }
                }, 2200);
            }
        }

        function mostrarMensagem(texto, cor) {
            const el = document.getElementById("msg");
            el.textContent = texto;
            el.style.color = cor;
            el.style.display = "block";
            setTimeout(() => el.style.display = "none", 2000);
        }

        function reiniciar() {
            if (onibus.corpo) {
                onibus.corpo.position.set(0, 0.3, 6);
                onibus.corpo.velocity.set(0, 0, 0);
                onibus.corpo.angularVelocity.set(0, 0, 0);
                onibus.corpo.quaternion.set(0, 0, 0, 1);
            }
            cena.userData.obstaculos.forEach(obj => { cena.remove(obj.malha); mundoFisica.removeBody(obj.corpo); });
            iniciarFase();
        }

        function animar() {
            requestAnimationFrame(animar);
            mundoFisica.step(1/60);
            if (onibus.modelo && onibus.corpo) {
                onibus.modelo.position.copy(onibus.corpo.position);
                onibus.modelo.quaternion.copy(onibus.corpo.quaternion);
            }
            controles.update();
            verificarEstacionamento();
            renderizador.render(cena, camera);
        }

        window.addEventListener("resize", () => {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderizador.setSize(window.innerWidth, window.innerHeight);
        });

        iniciarJogo();
    </script>
</body>
</html>

Game Source: 🚌 Estacionar Ônibus 3D Real

Creator: DriftPenguin96

Libraries: three

Complexity: complex (273 lines, 12.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: estacionar-nibus-3d-real-driftpenguin96" to link back to the original. Then publish at arcadelab.ai/publish.