<!
doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Mini FPS Target Trainer</title>
  <style>
    html,body{height:100%;margin:0;overflow:hidden;font-family:Inter,system-
ui,Segoe UI,Roboto,'Helvetica Neue',Arial}
    #ui{position:fixed;left:12px;top:12px;z-
index:20;background:rgba(0,0,0,0.5);color:#fff;padding:12px;border-
radius:8px;backdrop-filter:blur(4px)}
    #ui label{display:block;font-size:13px;margin-top:6px}
    #crosshair{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-
index:15;pointer-events:none;font-size:22px;color:#fff}
    #debug{position:fixed;right:12px;top:12px;z-
index:20;background:rgba(0,0,0,0.5);color:#0f0;padding:8px;border-radius:6px;font-
family:monospace}
    button,select,input{margin-top:6px}
    canvas{display:block}
  </style>
</head>
<body>
  <div id="ui">
    <div>
       <label>Gun
         <select id="gunSelect">
           <option value="pistol">Pistol (semi)</option>
           <option value="smg">SMG (auto)</option>
           <option value="rifle">Rifle (burst)</option>
         </select>
       </label>
       <label>Sensitivity <input id="sens" type="range" min="0.2" max="3"
step="0.05" value="1"> <span id="sensVal">1.00</span></label>
       <label>Recoil Intensity <input id="recoil" type="range" min="0" max="2"
step="0.05" value="1"> <span id="recoilVal">1.00</span></label>
       <label>Target Speed <input id="tgtSpeed" type="range" min="0.2" max="3"
step="0.1" value="1"> <span id="tgtSpeedVal">1.00</span></label>
       <div>
         <button id="spawn">Spawn Targets</button>
         <button id="reset">Reset</button>
       </div>
       <div style="margin-top:8px;font-size:13px;opacity:0.9">Click to lock pointer.
Left click to shoot. R to reload. Mouse wheel to change guns.</div>
    </div>
  </div>
  <div id="crosshair">+</div>
  <div id="debug">FPS: <span id="fps">0</span><br>Bullets: <span
id="bullets">0</span><br>Hits: <span id="hits">0</span><br>Spread: <span
id="spread">0</span></div>
  <script src="https://cdn.jsdelivr.net/npm/three@0.154.0/build/three.min.js"></
script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.154.0/examples/js/controls/
PointerLockControls.js"></script>
  <script>
    // Basic scene setup
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, innerWidth/innerHeight, 0.1,
1000);
    camera.position.set(0,1.6,0);
    const renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setSize(innerWidth,innerHeight);
    document.body.appendChild(renderer.domElement);
    // light
    const amb = new THREE.HemisphereLight(0xffffff,0x444444,0.8);
    scene.add(amb);
    const dir = new THREE.DirectionalLight(0xffffff,0.7);
    dir.position.set(5,10,7);
    scene.add(dir);
    // floor
    const floorGeo = new THREE.PlaneGeometry(200,200);
    const floorMat = new THREE.MeshStandardMaterial({color:0x222222});
    const floor = new THREE.Mesh(floorGeo,floorMat);
    floor.rotation.x = -Math.PI/2;
    scene.add(floor);
    // simple crosshair raycaster
    const raycaster = new THREE.Raycaster();
    // UI
    const sens = document.getElementById('sens'); const sensVal =
document.getElementById('sensVal');
    const recoilSlider = document.getElementById('recoil'); const recoilVal =
document.getElementById('recoilVal');
    const gunSelect = document.getElementById('gunSelect');
    const tgtSpeed = document.getElementById('tgtSpeed'); const tgtSpeedVal =
document.getElementById('tgtSpeedVal');
    const fpsEl = document.getElementById('fps'), bulletsEl =
document.getElementById('bullets'), hitsEl = document.getElementById('hits'),
spreadEl = document.getElementById('spread');
    sensVal.innerText = parseFloat(sens.value).toFixed(2);
    recoilVal.innerText = parseFloat(recoilSlider.value).toFixed(2);
    tgtSpeedVal.innerText = parseFloat(tgtSpeed.value).toFixed(2);
    sens.addEventListener('input',()=>sensVal.innerText =
parseFloat(sens.value).toFixed(2));
    recoilSlider.addEventListener('input',()=>recoilVal.innerText =
parseFloat(recoilSlider.value).toFixed(2));
    tgtSpeed.addEventListener('input',()=>tgtSpeedVal.innerText =
parseFloat(tgtSpeed.value).toFixed(2));
    // stats (simple)
    let lastTime = performance.now(), frames = 0, fps = 0;
    // pointer lock
    const controls = new THREE.PointerLockControls(camera, document.body);
    document.body.addEventListener('click', ()=>{ if(!controls.isLocked)
controls.lock(); });
    controls.addEventListener('lock',
()=>{ document.getElementById('ui').style.display='block'; });
    controls.addEventListener('unlock',
()=>{ document.getElementById('ui').style.display='block'; });
    // guns config
    const GUNS = {
       pistol: {fireMode:'semi',rpm:300,mag:12, reload:1.2, recoil:0.8, spread:0.8},
       smg: {fireMode:'auto',rpm:900,mag:40, reload:2.2, recoil:1.2, spread:1.4},
       rifle: {fireMode:'burst',rpm:700,mag:30, reload:2.5, recoil:1.6, spread:1.0}
    };
    let currentGun = GUNS[gunSelect.value];
    gunSelect.addEventListener('change', ()=>{ currentGun =
GUNS[gunSelect.value]; });
    // ammo
    let bullets = currentGun.mag, bulletsFired = 0, hits=0;
    // shooting control
    let firing=false; let lastShotTime=0; let canFire=true; let spread=0;
    function getTime(){return performance.now()/1000}
    function shoot(){
      const now = getTime();
      const delay = 60/currentGun.rpm;
      if(!canFire) return;
      if(now - lastShotTime < delay) return;
      if(bullets<=0) { /* click */ return; }
      lastShotTime = now;
      bullets--; bulletsFired++;
      bulletsEl.innerText = bulletsFired;
      // recoil: apply camera pitch and yaw slight offset
      const recoilAmount = currentGun.recoil * parseFloat(recoilSlider.value);
      camera.rotation.x -= THREE.MathUtils.degToRad( (0.5 + Math.random()*0.6) *
recoilAmount );
      camera.rotation.y += THREE.MathUtils.degToRad( (Math.random()-0.5) *
recoilAmount * 0.6 );
      // spread increases
      spread += currentGun.spread * 0.02 * parseFloat(recoilSlider.value);
      spread = Math.min(spread, 0.6);
      // raycast with spread
      const center = new THREE.Vector3();
      camera.getWorldDirection(center);
      // apply random spread
      const spreadAngle = spread;
      const randX = (Math.random()-0.5)*spreadAngle;
      const randY = (Math.random()-0.5)*spreadAngle;
      const dir = center.clone().applyAxisAngle(new THREE.Vector3(0,1,0),
randY).applyAxisAngle(new THREE.Vector3(1,0,0), randX);
      raycaster.set(camera.position, dir);
      const hitsArr = raycaster.intersectObjects(targets.map(t=>t.mesh));
      if(hitsArr.length>0){
        const hit = hitsArr[0];
        // find target
        const t = targets.find(x=>x.mesh===hit.object);
        if(t && !t.hit){
          t.hit=true; t.mesh.material.color.set(0x00ff00);
                hits++; hitsEl.innerText = hits;
            }
        }
    }
    // handle continuous fire
    function handleAutoFire(){
      if(currentGun.fireMode==='auto' && firing){ shoot(); }
    }
    // map mouse movement to camera rotation with sensitivity
    let yaw=0, pitch=0;
    document.addEventListener('mousemove', (e)=>{
      if(!controls.isLocked) return;
      const s = parseFloat(sens.value);
      const movementX = e.movementX || 0;
      const movementY = e.movementY || 0;
      camera.rotation.y -= movementX * 0.002 * s;
      camera.rotation.x -= movementY * 0.002 * s;
      camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2,
camera.rotation.x));
    });
    // keyboard
    document.addEventListener('keydown', (e)=>{
      if(e.code==='KeyR'){ bullets = currentGun.mag; }
      if(e.code==='Space'){ /* could be jump */ }
      if(e.code==='KeyV'){ /* toggle */ }
    });
    // mouse buttons
    document.addEventListener('pointerdown', (e)=>{
      if(e.button===0){ // left
        firing=true;
        if(currentGun.fireMode==='semi' || currentGun.fireMode==='burst') shoot();
      }
    });
    document.addEventListener('pointerup', (e)=>{ if(e.button===0)
firing=false; });
    // change gun with wheel
    document.addEventListener('wheel', (e)=>{
      const idx =
Array.from(gunSelect.options).findIndex(o=>o.value===gunSelect.value);
      if(e.deltaY>0) gunSelect.selectedIndex = Math.min(gunSelect.options.length-1,
idx+1);
      else gunSelect.selectedIndex = Math.max(0, idx-1);
      gunSelect.dispatchEvent(new Event('change'));
    }, {passive:true});
    // simple recoil recovery and spread decay
    function recoilRecovery(dt){
      // recover camera pitch gradually
      camera.rotation.x += dt * 1.5; // recovery speed
      if(camera.rotation.x > 1.6) camera.rotation.x = 1.6;
      spread = Math.max(0, spread - dt*0.6);
    }
    // targets system
    const targets = [];
    function createTarget(x,y,z){
      const geo = new THREE.SphereGeometry(0.25,16,12);
      const mat = new THREE.MeshStandardMaterial({color:0xff4444, metalness:0.2,
roughness:0.6});
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(x,y,z);
      scene.add(mesh);
      targets.push({mesh, baseX:x, baseY:y, baseZ:z, t:Math.random()*10,
hit:false});
    }
    function spawnTargets(n=6){
      // clear existing
      targets.forEach(t=>scene.remove(t.mesh)); targets.length=0;
      for(let i=0;i<n;i++){
        const x = (Math.random()-0.5)*20 + 0; const y = 1 + Math.random()*2; const
z = -10 - Math.random()*20;
        createTarget(x,y,z);
      }
      bullets = currentGun.mag; bulletsFired = 0; hits = 0; bulletsEl.innerText =
bulletsFired; hitsEl.innerText = hits;
    }
    document.getElementById('spawn').addEventListener('click',
()=>spawnTargets(8));
    document.getElementById('reset').addEventListener('click',
()=>{ spawnTargets(6); camera.rotation.set(0,0,0); spread=0; });
    spawnTargets(6);
    // animation loop
    let prev = performance.now()/1000;
    function animate(){
      requestAnimationFrame(animate);
      const nowMs = performance.now(); frames++; const dtMs = nowMs - lastTime;
      if(nowMs - lastTime >= 250){ fps = Math.round((frames*1000)/(nowMs -
lastTime)); lastTime = nowMs; frames=0; fpsEl.innerText = fps; }
      const now = performance.now()/1000; const dt = Math.min(now - prev, 0.05);
prev = now;
      // move targets
      const ts = parseFloat(tgtSpeed.value);
      targets.forEach((t,i)=>{
        t.t += dt * ts;
        t.mesh.position.x = t.baseX + Math.sin(t.t*1.2 + i) * (1.5 +
Math.sin(i))*1.5;
        t.mesh.position.y = t.baseY + Math.sin(t.t*1.7 + i) * 0.6;
      });
      // firing for auto
      handleAutoFire();
      // recoil recovery
      recoilRecovery(dt);
      // update spread display
      spreadEl.innerText = spread.toFixed(3);
      renderer.render(scene,camera);
    }
    animate();
    // resize
    window.addEventListener('resize', ()=>{ camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
    // simple click-to-spawn targets on hit (for practice)
    // (no additional code needed for now)
    // extra: simple audio feedback (optional)
    // omitted to keep single-file small
    // keep UI visible
    document.getElementById('ui').style.display='block';
  </script>
</body>
</html>