Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Game-Unknown | 3D Sandbox</title> | |
| <!-- Fonts --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Rajdhani:wght@300;500;700&display=swap" rel="stylesheet"> | |
| <!-- Three.js and Addons --> | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", | |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" | |
| } | |
| } | |
| </script> | |
| <style> | |
| :root { | |
| --primary: #00f260; | |
| --secondary: #0575e6; | |
| --glass: rgba(255, 255, 255, 0.1); | |
| --glass-border: rgba(255, 255, 255, 0.2); | |
| --text: #ffffff; | |
| --bg-gradient: linear-gradient(135deg, #0f0c29, #302b63, #24243e); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| user-select: none; | |
| } | |
| body { | |
| font-family: 'Rajdhani', sans-serif; | |
| background: var(--bg-gradient); | |
| color: var(--text); | |
| overflow: hidden; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| /* --- Canvas --- */ | |
| #game-canvas { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 1; | |
| display: none; /* Hidden until start */ | |
| } | |
| /* --- UI Overlay --- */ | |
| #ui-layer { | |
| position: relative; | |
| z-index: 10; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; /* Let clicks pass to canvas when locked */ | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| /* --- Header --- */ | |
| header { | |
| padding: 20px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| background: linear-gradient(to bottom, rgba(0,0,0,0.8), transparent); | |
| pointer-events: auto; | |
| } | |
| .logo { | |
| font-family: 'Orbitron', sans-serif; | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| background: linear-gradient(to right, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: rgba(255,255,255,0.7); | |
| text-decoration: none; | |
| border-bottom: 1px solid var(--primary); | |
| transition: 0.3s; | |
| } | |
| .anycoder-link:hover { color: var(--primary); } | |
| /* --- Setup Screen (Modal) --- */ | |
| #setup-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(15, 12, 41, 0.9); | |
| backdrop-filter: blur(10px); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 20; | |
| pointer-events: auto; | |
| transition: opacity 0.5s ease; | |
| } | |
| .panel { | |
| background: var(--glass); | |
| border: 1px solid var(--glass-border); | |
| padding: 40px; | |
| border-radius: 20px; | |
| width: 90%; | |
| max-width: 500px; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| } | |
| h2 { | |
| font-family: 'Orbitron', sans-serif; | |
| margin-bottom: 20px; | |
| text-align: center; | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| input[type="text"], select, input[type="file"] { | |
| width: 100%; | |
| padding: 12px; | |
| background: rgba(0, 0, 0, 0.5); | |
| border: 1px solid var(--glass-border); | |
| color: white; | |
| border-radius: 8px; | |
| font-family: 'Rajdhani', sans-serif; | |
| font-size: 1.1rem; | |
| outline: none; | |
| transition: 0.3s; | |
| } | |
| input:focus, select:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 10px rgba(0, 242, 96, 0.3); | |
| } | |
| .btn { | |
| width: 100%; | |
| padding: 15px; | |
| border: none; | |
| border-radius: 8px; | |
| background: linear-gradient(90deg, var(--secondary), var(--primary)); | |
| color: #000; | |
| font-family: 'Orbitron', sans-serif; | |
| font-weight: 700; | |
| font-size: 1.2rem; | |
| cursor: pointer; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| text-transform: uppercase; | |
| } | |
| .btn:hover { | |
| transform: scale(1.02); | |
| box-shadow: 0 0 20px rgba(0, 242, 96, 0.6); | |
| } | |
| /* --- HUD --- */ | |
| #hud { | |
| display: none; /* Hidden initially */ | |
| padding: 20px; | |
| pointer-events: auto; | |
| } | |
| .hud-top { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| } | |
| .crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 20px; | |
| height: 20px; | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| } | |
| .crosshair::before, .crosshair::after { | |
| content: ''; | |
| position: absolute; | |
| background: var(--primary); | |
| } | |
| .crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; } | |
| .crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; } | |
| .controls-hint { | |
| text-align: center; | |
| font-size: 0.9rem; | |
| opacity: 0.7; | |
| margin-top: 10px; | |
| } | |
| /* --- Notifications --- */ | |
| #notifications { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| z-index: 30; | |
| } | |
| .toast { | |
| background: rgba(0,0,0,0.8); | |
| border-left: 4px solid var(--primary); | |
| padding: 15px; | |
| border-radius: 4px; | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| /* Weapon Model */ | |
| #weapon-container { | |
| position: absolute; | |
| bottom: 0; | |
| right: 20%; | |
| width: 200px; | |
| height: 200px; | |
| display: none; /* Hidden until game starts */ | |
| pointer-events: none; | |
| } | |
| /* Loading Spinner */ | |
| .loader { | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-left-color: var(--primary); | |
| border-radius: 50%; | |
| width: 30px; | |
| height: 30px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto; | |
| display: none; | |
| } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- 3D Canvas --> | |
| <canvas id="game-canvas"></canvas> | |
| <!-- UI Layer --> | |
| <div id="ui-layer"> | |
| <header> | |
| <div class="logo">Game-Unknown</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with AnyCoder</a> | |
| </header> | |
| <!-- HUD --> | |
| <div id="hud"> | |
| <div class="hud-top"> | |
| <div id="player-health">HP: 100%</div> | |
| <div id="world-name">World: Greek City</div> | |
| </div> | |
| <div class="crosshair"></div> | |
| <div class="controls-hint">WASD to Move | SPACE to Jump | CLICK to Shoot | ESC to Pause</div> | |
| </div> | |
| </div> | |
| <!-- Weapon Overlay (3D Model attached to camera later) --> | |
| <div id="weapon-container"></div> | |
| <!-- Setup Screen --> | |
| <div id="setup-screen"> | |
| <div class="panel"> | |
| <h2>Mission Init</h2> | |
| <div class="form-group"> | |
| <label>HF Token (Authentication)</label> | |
| <input type="text" id="hf-token" placeholder="Enter HuggingFace Token..."> | |
| </div> | |
| <div class="form-group"> | |
| <label>Load Custom Model (.glb / .gltf)</label> | |
| <input type="file" id="model-upload" accept=".glb, .gltf"> | |
| <div style="text-align: center; margin: 5px 0; font-size: 0.8rem;">OR</div> | |
| <select id="preset-model"> | |
| <option value="default">Select Preset: Soldier</option> | |
| <option value="wizard">Preset: Wizard</option> | |
| <option value="cyborg">Preset: Cyborg</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>World Environment</label> | |
| <select id="world-select"> | |
| <option value="greek">Greek City (Ruins)</option> | |
| <option value="warehouse">Abandoned Warehouse</option> | |
| <option value="cyber">Neon Cyber Field</option> | |
| </select> | |
| </div> | |
| <div class="loader" id="loader"></div> | |
| <button class="btn" id="enter-btn">ENTER SIMULATION</button> | |
| </div> | |
| </div> | |
| <div id="notifications"></div> | |
| <script type="module"> | |
| import * as THREE from 'three'; | |
| import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'; | |
| import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; | |
| // --- State & Config --- | |
| const state = { | |
| moveForward: false, | |
| moveBackward: false, | |
| moveLeft: false, | |
| moveRight: false, | |
| canJump: false, | |
| velocity: new THREE.Vector3(), | |
| direction: new THREE.Vector3(), | |
| isPlaying: false, | |
| world: 'greek' | |
| }; | |
| // --- Scene Setup --- | |
| const canvas = document.querySelector('#game-canvas'); | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x050510); | |
| scene.fog = new THREE.FogExp2(0x050510, 0.02); | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.y = 1.6; // Eye level | |
| const renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| // --- Lighting --- | |
| const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6); | |
| scene.add(hemiLight); | |
| const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| dirLight.position.set(10, 20, 10); | |
| dirLight.castShadow = true; | |
| dirLight.shadow.camera.top = 50; | |
| dirLight.shadow.camera.bottom = -50; | |
| dirLight.shadow.camera.left = -50; | |
| dirLight.shadow.camera.right = 50; | |
| scene.add(dirLight); | |
| // --- Controls --- | |
| const controls = new PointerLockControls(camera, document.body); | |
| // --- Weapon Visuals (Procedural Gun) --- | |
| const weaponGeo = new THREE.BoxGeometry(0.2, 0.2, 0.6); | |
| const weaponMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.2, metalness: 0.8 }); | |
| const weapon = new THREE.Mesh(weaponGeo, weaponMat); | |
| weapon.position.set(0.3, -0.3, -0.5); | |
| camera.add(weapon); | |
| scene.add(camera); // Add camera to scene so weapon renders | |
| // --- Physics / Movement Logic --- | |
| let prevTime = performance.now(); | |
| const onKeyDown = function (event) { | |
| switch (event.code) { | |
| case 'ArrowUp': case 'KeyW': state.moveForward = true; break; | |
| case 'ArrowLeft': case 'KeyA': state.moveLeft = true; break; | |
| case 'ArrowDown': case 'KeyS': state.moveBackward = true; break; | |
| case 'ArrowRight': case 'KeyD': state.moveRight = true; break; | |
| case 'Space': if (state.canJump === true) state.velocity.y += 350; state.canJump = false; break; | |
| } | |
| }; | |
| const onKeyUp = function (event) { | |
| switch (event.code) { | |
| case 'ArrowUp': case 'KeyW': state.moveForward = false; break; | |
| case 'ArrowLeft': case 'KeyA': state.moveLeft = false; break; | |
| case 'ArrowDown': case 'KeyS': state.moveBackward = false; break; | |
| case 'ArrowRight': case 'KeyD': state.moveRight = false; break; | |
| } | |
| }; | |
| document.addEventListener('keydown', onKeyDown); | |
| document.addEventListener('keyup', onKeyUp); | |
| // --- Mouse Click (Shoot) --- | |
| document.addEventListener('mousedown', () => { | |
| if (state.isPlaying && controls.isLocked) { | |
| // Visual Recoil | |
| weapon.position.z = -0.3; | |
| setTimeout(() => { weapon.position.z = -0.5; }, 100); | |
| // Simple Raycast to see if we hit the 'enemies' | |
| const raycaster = new THREE.Raycaster(); | |
| raycaster.setFromCamera(new THREE.Vector2(0,0), camera); | |
| // Find targets | |
| const intersects = raycaster.intersectObjects(enemies.children); | |
| if(intersects.length > 0) { | |
| const hitObj = intersects[0].object; | |
| // "Kill" effect | |
| hitObj.material.color.setHex(0xff0000); | |
| scene.remove(hitObj); | |
| notify("Target Eliminated"); | |
| } | |
| } | |
| }); | |
| // --- World Generation --- | |
| const enemies = new THREE.Group(); | |
| scene.add(enemies); | |
| function createEnvironment(type) { | |
| // Clear previous | |
| while(scene.children.length > 0){ | |
| const obj = scene.children[0]; | |
| if(obj.type === "Mesh" || obj.type === "Group") { | |
| if(obj !== camera && obj !== enemies) scene.remove(obj); | |
| } | |
| } | |
| enemies.clear(); | |
| // Floor | |
| let floorGeo, floorMat; | |
| if(type === 'cyber') { | |
| floorGeo = new THREE.PlaneGeometry(200, 200, 100, 100); | |
| floorMat = new THREE.MeshStandardMaterial({ color: 0x000000, wireframe: true, emissive: 0x0575e6, emissiveIntensity: 0.5 }); | |
| } else { | |
| floorGeo = new THREE.PlaneGeometry(200, 200); | |
| floorMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.8 }); | |
| } | |
| const floor = new THREE.Mesh(floorGeo, floorMat); | |
| floor.rotation.x = - Math.PI / 2; | |
| floor.receiveShadow = true; | |
| scene.add(floor); | |
| // Procedural Props based on World Type | |
| if (type === 'greek') { | |
| for(let i=0; i<50; i++) { | |
| const colGeo = new THREE.CylinderGeometry(1, 1, 10, 6); | |
| const colMat = new THREE.MeshStandardMaterial({ color: 0xdddddd }); | |
| const column = new THREE.Mesh(colGeo, colMat); | |
| column.position.set((Math.random()-0.5)*100, 5, (Math.random()-0.5)*100); | |
| column.castShadow = true; | |
| column.receiveShadow = true; | |
| scene.add(column); | |
| // Spawn Enemy nearby | |
| spawnEnemy(column.position); | |
| } | |
| } else if (type === 'warehouse') { | |
| for(let i=0; i<20; i++) { | |
| const boxGeo = new THREE.BoxGeometry(5, 5, 5); | |
| const boxMat = new THREE.MeshStandardMaterial({ color: 0x555555 }); | |
| const box = new THREE.Mesh(boxGeo, boxMat); | |
| box.position.set((Math.random()-0.5)*80, 2.5, (Math.random()-0.5)*80); | |
| box.castShadow = true; | |
| scene.add(box); | |
| spawnEnemy(box.position); | |
| } | |
| } else { | |
| // Cyber / Generic | |
| for(let i=0; i<40; i++) { | |
| const geo = new THREE.TorusGeometry(2, 0.5, 8, 20); | |
| const mat = new THREE.MeshStandardMaterial({ color: 0x00f260, emissive: 0x00f260, emissiveIntensity: 0.8 }); | |
| const ring = new THREE.Mesh(geo, mat); | |
| ring.position.set((Math.random()-0.5)*100, 3, (Math.random()-0.5)*100); | |
| ring.rotation.x = Math.PI / 2; | |
| scene.add(ring); | |
| spawnEnemy(ring.position); | |
| } | |
| } | |
| } | |
| function spawnEnemy(refPos) { | |
| const geo = new THREE.SphereGeometry(1, 32, 32); | |
| const mat = new THREE.MeshStandardMaterial({ color: 0xff0000 }); | |
| const enemy = new THREE.Mesh(geo, mat); | |
| // Random position near reference | |
| enemy.position.set( | |
| refPos.x + (Math.random()-0.5)*20, | |
| 2, | |
| refPos.z + (Math.random()-0.5)*20 | |
| ); | |
| enemy.userData = { moveDir: Math.random() }; | |
| enemies.add(enemy); | |
| } | |
| // --- Model Loading Logic --- | |
| function loadPlayerModel(source, file = null) { | |
| // Remove existing child (default camera child is weapon, we want to attach model to a group parented to camera) | |
| // For this demo, we will just change the camera height or simple geometry | |
| // Implementing full GLTF loading requires actual file URLs. | |
| // Since this is a text response, we simulate the upload process. | |
| if (file) { | |
| const url = URL.createObjectURL(file); | |
| const loader = new GLTFLoader(); | |
| loader.load(url, function (gltf) { | |
| const model = gltf.scene; | |
| model.scale.set(1, 1, 1); // Adjust scale | |
| // Ideally attach to a wrapper group at camera position | |
| notify("Custom Model Loaded Successfully"); | |
| }, undefined, function (error) { | |
| console.error(error); | |
| notify("Error loading model. Using Default."); | |
| }); | |
| } else { | |
| notify(`Loaded Preset: ${source}`); | |
| } | |
| } | |
| // --- UI Interactions --- | |
| const setupScreen = document.getElementById('setup-screen'); | |
| const hud = document.getElementById('hud'); | |
| const enterBtn = document.getElementById('enter-btn'); | |
| const worldSelect = document.getElementById('world-select'); | |
| const modelUpload = document.getElementById('model-upload'); | |
| const presetModel = document.getElementById('preset-model'); | |
| const loader = document.getElementById('loader'); | |
| enterBtn.addEventListener('click', () => { | |
| const token = document.getElementById('hf-token').value; | |
| if(!token) { | |
| alert("Please enter a HuggingFace Token (or any text to proceed)."); | |
| return; | |
| } | |
| loader.style.display = 'block'; | |
| enterBtn.style.opacity = '0.5'; | |
| // Simulate Loading | |
| setTimeout(() => { | |
| state.world = worldSelect.value; | |
| loadPlayerModel(presetModel.value, modelUpload.files[0]); | |
| createEnvironment(state.world); | |
| loader.style.display = 'none'; | |
| setupScreen.style.opacity = '0'; | |
| setTimeout(() => { | |
| setupScreen.style.display = 'none'; | |
| hud.style.display = 'block'; | |
| canvas.style.display = 'block'; | |
| controls.lock(); | |
| state.isPlaying = true; | |
| }, 500); | |
| }, 1500); | |
| }); | |
| // Handle Pointer Lock Events | |
| controls.addEventListener('lock', function () { | |
| state.isPlaying = true; | |
| }); | |
| controls.addEventListener('unlock', function () { | |
| state.isPlaying = false; | |
| // Show pause menu logic could go here | |
| notify("Paused - Click to Resume"); | |
| }); | |
| function notify(msg) { | |
| const container = document.getElementById('notifications'); | |
| const el = document.createElement('div'); | |
| el.className = 'toast'; | |
| el.innerText = msg; | |
| container.appendChild(el); | |
| setTimeout(() => el.remove(), 3000); | |
| } | |
| // --- Main Loop --- | |
| function animate() { | |
| requestAnimationFrame(animate); | |
| if (state.isPlaying) { | |
| const time = performance.now(); | |
| const delta = (time - prevTime) / 1000; | |
| // Movement Physics | |
| state.velocity.x -= state.velocity.x * 10.0 * delta; | |
| state.velocity.z -= state.velocity.z * 10.0 * delta; | |
| state.velocity.y -= 9.8 * 100.0 * delta; // Gravity | |
| state.direction.z = Number(state.moveForward) - Number(state.moveBackward); | |
| state.direction.x = Number(state.moveRight) - Number(state.moveLeft); | |
| state.direction.normalize(); | |
| if (state.moveForward || state.moveBackward) state.velocity.z -= state.direction.z * 400.0 * delta; | |
| if (state.moveLeft || state.moveRight) state.velocity.x -= state.direction.x * 400.0 * delta; | |
| controls.moveRight(- state.velocity.x * delta); | |
| controls.moveForward(- state.velocity.z * delta); | |
| camera.position.y += (state.velocity.y * delta); // Simple gravity | |
| // Floor Collision | |
| if (camera.position.y < 1.6) { | |
| state.velocity.y = 0; | |
| camera.position.y = 1.6; | |
| state.canJump = true; | |
| } | |
| // Animate Enemies (Bobbing) | |
| enemies.children.forEach((child, idx) => { | |
| child.position.y = 2 + Math.sin(time * 0.003 + idx) * 0.5; | |
| child.rotation.y += 0.01; | |
| }); | |
| prevTime = time; | |
| } | |
| renderer.render(scene, camera); | |
| } | |
| // Window Resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| }); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |