print('Hello world!')```html
<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>전술 FPS 프로토타입</title>
  <style>
    * { box-sizing: border-box; }
    body {
      margin: 0;
      overflow: hidden;
      background: #10151d;
      color: #f5f7fa;
      font-family: system-ui, sans-serif;
    }
    canvas { display: block; }
    #hud {
      position: fixed;
      inset: 0;
      pointer-events: none;
      text-shadow: 0 2px 5px #000;
    }
    #top-left {
      position: absolute;
      top: 18px;
      left: 20px;
      font-size: 14px;
      line-height: 1.7;
    }
    #score {
      color: #67e8c1;
      font-size: 20px;
      font-weight: 800;
    }
    #help {
      position: absolute;
      bottom: 18px;
      left: 50%;
      transform: translateX(-50%);
      color: #d9e1ec;
      font-size: 13px;
      text-align: center;
      white-space: nowrap;
    }
    #crosshair {
      position: absolute;
      top: 50%;
      left: 50%;
      width: 18px;
      height: 18px;
      transform: translate(-50%, -50%);
    }
    #crosshair::before, #crosshair::after {
      content: "";
      position: absolute;
      background: #fff;
      box-shadow: 0 0 3px #000;
    }
    #crosshair::before { width: 2px; height: 18px; left: 8px; top: 0; }
    #crosshair::after { width: 18px; height: 2px; left: 0; top: 8px; }
    #hitmark {
      position: absolute;
      left: 50%;
      top: 50%;
      color: #ff6464;
      font-size: 30px;
      font-weight: 800;
      opacity: 0;
      transform: translate(-50%, -55%);
    }
    #overlay {
      position: fixed;
      inset: 0;
      display: grid;
      place-items: center;
      background: rgba(```html
<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-7,8"> 12, 20
  <meta, name=" 0.78viewport" content);="
      textwidth=device-width,-align: center initial-scale;=1">
  <title>
    }
    #전술 FPSpanel { 프로
      width토:타입 min(</title440>px
  <style,>
    * calc {(100 box-sizing%: border --box```; } 32html
<!DOCTYPEpx
    body html {>));
<html lang
      padding:="
      margin: 0;ko
      overflow 30">
<headpx>;:
  <meta
      border charset: 1=" hiddenUTF-8";px
      background /> solid: # #536
  <meta273 name="101;viewport" content="
      border51width-radius=device-width,: 14d initial-scale=px1;.;
      color0":
      background />: rgba(
  <title> white17;전,
      font술-family 26 FPS:, system 프로 38-ui, sans토,타입-serif;
    } 0.</title>
    canvas
  <style { display96>: block
    *);; { } box-sizing
    #:
      box-shadow border:-boxhud; } 0 {
      position:
    body 18 fixed; {px
      inset
      margin 60:: 0;px 16px
      overflow: hidden #;000
      background 18: #px8 auto;;
    }10151b;
      display
      color:: # flex;
    h1fff { margin;
      justify-content
      font:-family: space 0:-between system; 0-ui
      pointer, sans-events-serif: none;;
    } 10
      textpx-shadow;
    canvas { display:: block font 0-size;: } 2
    # 28pxhud { 5px;px }
      position #:000 fixed
    #;;
      font
      inset-weight:panel 0 p {:; 700
      pointer-events color;:: #
    }c
    # none;1ccd8;message
      text-shadow {: line 0
      position-height: 2: fixed;px 1. 47;px }
      left #:000 50;
    button {
    }
    #%;
      top:top {
      position: 50%;
      margin absolute
      transform;
      top-top:: translate(-50 12%,:px -; 1850px%);;
      padding:
      left: 12 50%;
      padding:px
      transform: 24 translateX 24pxpx(-50%);;
      border: 30px
      padding;: 0
      max; 10-width:px
      border min-radius 18:(90vwpx; 7
      border,:px; 480px 1);px solid
      text
      background-align: center: # #67;e8c1ffffff;
      line-height
      color:35: #; 1
      background.7;: #102101
      background:51 #01bcc;
      border-radius101d:; 8px
      font51;-size
      font-weight:de 16:8;px
      border: 700;;
      letter
      font-weight-spacing:: . 800 1;px04 solid #
      cursor:em6;
    } pointer
    #;
    }bdstats {
  </style>
      position8:ff; absolute;
      border
      left:
</head>-radius:
<body 12 24>px
  <divpx;;
      bottom
      box id:="-shadow: 24hud">px 0; 0
      display
    <div: id=" 30 flex;px #
      gap000:top-left"> 18px;8
      <div>
      font;-weight
    }
    #: 700message;점수
      font button-size {: <span id="
      margin 18scorepx;-top:">
    }0
    # 12</spanpxmessage;> {</div>
      position
      padding: 10:
      <div> absolute;px
      top:탄 20 58px;%;약
      left:
      border: 0; 50%; <span
      border id-radius
      transform=": translate(-:ammo50%, 6 -px50">;%);
      color
      text:12 #-align: center; /
      font081-size: 12 18</spanpx119;></div>;
      line-height
      background::
      <div #> 1.6bd88표;ff;적 <span
      color
      font: # id="-weightfff:; 800
      background:target;
      cursor #: pointer101;51
    }-countbd">
    #cross90</spanhair;> {
      border
      position: 1</div:> fixed;px solid #
      leftffffff
    </div40:>; 50
      border
    <div%; id="
      topcross-radius:: 10 50hairpx%;;">
      padding</div:
      width>: 18
    <div 16 id="pxpx; 26
      height:pxhit 16;mark">
      minpx×-width;:</div> 300
      transform:px; translate(-50
    <div id
    }="
    #%, -50%);cross
      pointerhelp">hair-events {:
      positionW none;:ASD absolute 이동;
    } ·
      left
    #:cross 50%; 마hair::before우스
      top, #: 50cross 조hair%;준::after · 클릭 {
      width: 사 18
      contentpx격: · "";;
      height
      position R:: 18 재px absolute장;전; ·
      background:
      transform white: ESC translate; 메뉴(-
      box50-shadow%, -</div>: 050%);
  </div
    }> 0
    #cross

  <div id=" 3hairpx:: #000;before, #
    }overlaycross
    #">hair::after
    <div id=" {crosshair
      content::: "";beforepanel { width
      position:">: absolute
      <h;1>
      background전: 2 #px; height술e:aff 사격 16ff;px; left
      box:-shadow: 훈 7련px 0 0; }</h1
    #> 3pxcross #000
      <p;>hair::
    }after
    # {장crosshair::before { width:애 2px; height:물이 18px; left: 8px 놓인; }
    #cross 훈련hair::after {장에서 표적 height을: 2 찾아px 사격; width:하세요 18px;.<br top>: 8px; }헤
    #드hit {
      position샷:은 absolute 한;
      left: 번에 50%; 처
      top치:할 50%; 수
      width 있습니다:.</p 28>px;
      height: 28
      <buttonpx id="start;
      transform-button">: translate(-50%, -훈50련 시작%) rotate</button>(45deg);
      opacity:
    </div> 0;
    }
  </div
    #>hit::before, #hit

  <script:: type="after {
      content: "";module">
    import * as THREE from "
      positionhttps://: absolute;
      background: #ff4cdnd4d;
    }.jsdelivr
    #.nethit::before { width:/npm/three@ 20.px; height: 28px160; left.:0 13/build/px;three }
    #.modulehit::after { width:.js";

    const 28px; height: scene 2 = newpx THREE;.Scene top:();
    scene 13px; }.background
    . = new THREE.Color(0hiddenx { display111: none;a24); }
    scene.f
  </styleog> =
</head new> THREE.F
<body>og
  <div id(="0xhud111">a24
    <div, id="top"> 35훈,련장 75 ·); 전술

    const FPS camera = 프로 new THREE토타입.P</divers>pective
    <divCamera id="(75,crosshair"></div innerWidth> /
    <div innerHeight id=",hit"></div> 0.1,
    <div id="stats 100);">
    camera.position
      <span.set>(0,체력 1. <b id="65,health 8);">100</b></span>
    camera
      <span.rotation>.order처 = "YX치 <b id="Z";score">

    const0</b> renderer =</span> new THREE.Web
      <span>적GLRenderer <b id="({ antialremainingias: true">5 });</b>
    renderer</span>.set
    </divSize>(innerWidth,
    <div id inner="message">
      클릭Height);
    renderer해서.set 시작Pixel<br />Ratio(Math.min(devicePixel
      <small>WRatioASD 이동 ·, 2)); 마
    renderer우스 조.shadow준 · 클릭 사Map격 · ESC.enabled 일 =시 true;정지</small
    renderer>.shadowMap
    </div>.type = THREE
  </div.P>CF

  <script type="Softmodule">Shadow
    import *Map; as THREE
    document from ".body.appendChildhttps://(renderercdn.jsdelivr.net/npm.dom/three@0.Element);160

    scene.0/build.add/three(new THREE.H.moduleem.js";

    constisphere scene = new THREE.SceneLight(0();
    scene.backgroundxb = new THREE.Color(0x991da9b82);
    scene.fffog, = new 0 THREEx.Fog(2630x91a039a,b2, 2. 24,1)); 75

    const);

    const sun camera = = new new THREE THREE.P.DersirectionalLight(pective0Cameraxffffff,( 2.
      75, window2.innerWidth); /
    sun window.position.innerHeight.set(-,8 0,.1, 16, 120
    );
    camera.position.set 7(0,); 1
    sun.7.cast, 8);Shadow = true

    const; renderer = new THREE
    scene.Web.add(sGLunRenderer({); ant

    constial floorias: true }); =
    renderer new THREE.Mesh.set(Size(window.inner
      new THREEWidth,.Pl window.innerHeightane);Geometry(
    renderer50.set,PixelRatio(Math.min 70),
      new(window THREE.devicePixel.MeshRatioStandard,Material 2));({ color:
    renderer.shadowMap 0.enabledx = true;364
    renderer.shadowMap.type =34 THREEd.P, roughnessCF: 0Soft.Shadow92Map;
    document })
    );.body.appendChild(renderer
    floor.dom.rotation.x = -MathElement.PI);

    scene /.add 2;(new THREE.Hem
    floor.positionisphere.set(0,Light(0 0,xd -de10);
    flooreff.receive, 0xShadow394 = true;34
    scene.add(faloor,);

    const 2)); grid = new THREE

    const sun.Grid =Helper( new50 THREE.Directional,Light(0xffffff 50, 2., 0x2);
    sun.position718.set(-38d,8, 0 16x,4 7b);
    sun5b.cast65);
    gridShadow.position = true;
    sun.shadow.set.mapSize(0.set,(102 0.4, 1020124);, -
    scene10.add);(sun);
    scene.add

    const ground(grid);

    const = new THREE.Mesh wall(
      new THREEMaterial =.Pl new THREEaneGeometry(.Mesh60StandardMaterial,({ 60), color:
      new 0 THREE.MeshStandardxMaterial({ color: 0586x5664536d,a roughness:, 0. roughness:95 0. })
    );85
    ground.rotation.x }); = -Math

    function.PI / add 2;Box
    ground(x, y,.receiveShadow z, width, = true;
    scene height.add(,ground depth);

    const arena =, material = new THREE.Group(); wall
    sceneMaterial.add(ar)ena {);
      const box =

    function addBox(x new, THREE.Mesh(new y, z, THREE w, h,.Box d,Geometry(width color), { height,
      const depth), mesh material =); new THREE
      box.Mesh(
        new THREE.position.set(x,.Box yGeometry(w,, h, d), z);
        new THREE.Mesh
      boxStandard.castShadow =Material({ color, roughness: true; 0.
      box85.receive })
      );Shadow = true
      mesh.position.set;(x, y,
      scene.add z);(box
      mesh);
      return.castShadow box =; true;
    }
      mesh.receiveShadow

    // = true;
      arena.add(mesh); 훈련
      return mesh장;
    }

    // 경계
    addBox(- 훈련장 외24,벽 3, -
    addBox(0,10, 2, - 1, 6,20, 40, 58 4,);
    add 1Box,( 024, 3, -10,x 165747,a); 6,
    addBox(0, 2, 58); 20
    addBox(, 40, 40,, 1, 3 0, -x65747a);
    add39Box(-,20, 48 2,, 6, 0, 1, 1); 4,
    addBox(0, 40, 0x65747a 3);,
    addBox(20, 19 2,, 48 0,, 1, 6, 4, 1);

    // 40, 0x65747a 시);야

    // 엄를 가리는폐물
    [ 엄
      [-폐물
    addBox(-7, 1,8, - 1.1, -5,8 3,, 2, 3 3,, 2.2 0,x8b 2.765e],
      [55);, 1.
    addBox(4, -8,7, 1.5 2,. -515, 2.,8, 2. 2.5,6, 3, 0x6f817 2.6e);],
    add
      [Box(-8, 1, 32,, 1, - 4, 2,23, 2, 4, 2, 0x 2);827
    addBox45(f12],,
      [- 14, 1..2, -292,, 7 3,, 2. 2.5,4, 2.4, 3); 3,
    addBox 0(-x13758,17 1.b4, -],
      [0,32 0., 2.7,5 -,13 2.,8, 5, 1 3.);4,

    const 2, targets 0 = [];x
    const max867bTargets =67 5;],
      [-
    const13 ray,caster = 1 new, THREE 1,.Ray 2,caster 2,();
    const 5, keys 0 =x new Set6();
    conste scoreElement =7 document.getd78],Element
      [13,ById 1(", -score7,"); 2,
    const 2, ammoElement 4 =, document.getElement 0ById("x8aammo");765
    conste target]CountElement
    ]. =forEach(box => add documentBox.get(...Elementbox));ById("target

    const enemies-count = [];");
    const
    const overlay enemy =Colors document =.get [0ElementById("xe34overlayf");
    const55, hit 0markxd = document84a55.get, 0ElementByxefId("hit6858mark, 0");xc

    let944 score = 050;
    let ammo, 0xe =453 12;68];
    let

    function spawnEnemy yaw =(index) 0; {
    let
      const pitch group = = 0 new; THREE.Group();
      const body
    let lastShot =Mat 0; = new THREE.MeshStandard
    letMaterial hit({
        color:markTimer = enemyColors[index % 0 enemy;
    letColors.length], reload
        roughTimer =ness: 0 0.;7
      });
      const

    function dark updateMat = new THREE.MeshHUDStandardMaterial({() { color:
      score 0Element.textxContent272 = score;d
      ammo35Element });.textContent
      const body = = `${ newammo THREE}.Mesh(new / THREE 12`;.BoxGeometry(0.
      targetCountElement.text72Content = targets, 1..length;
    }05, 0.

    function spawnTarget42), bodyMat() {);
      const
      body.position group.y = = 0 new. THREE.Group();88;
      body.cast
      constShadow x = = true;
      const head = new THREE.Mesh THREE(.MathUtils
        new.rand THREE.SFloat(-phereGeometry(0.1728,, 17 16,); 12
      const z =),
        new THREE.MeshStandardMaterial({ color: THREE 0.MathUtils.randFloat(-xd7aa358f })
      );
      head,.position -7);.y = 1.
      group.position.set(x,62 0,; z
      head.cast);Shadow = true;

      const bodyMaterial
      const legs = = new new THREE THREE.Mesh.Mesh(new THREE.BoxGeometry(0.62, 0.5, 0.35StandardMaterial({
        color: 0xd14), darkd54Mat,);
        rough
      legsness.position.y: = 0. 0.5525;,
      legs.cast
        metalnessShadow = true;: 0.

      for (08const part
      }); of [body,
      const headMaterial head, legs =]) {
        part new THREE.Mesh.userStandardData.enemyMaterial =({ group;
        color:
        group 0.addxf(part0);c
      }

      const angle =19 Math.random()b, * Math.PI
        rough * 2;ness:
      const 0. radius = 7 +7 Math.random
      });() * 8;

      const body
      group.position =.set new THREE(Math.Mesh.cos(angle() * radius
        new, THREE 0,.CylinderGeometry( Math.sin(angle)0 *.36 radius, - 0. 2);
      group48.user,Data = 1. {
        health:15, 100, 12
        attack),
        bodyMaterial
      );Timer: Math
      body.position.y.random() * = 1.5, 0.
        alive:85 true;
      };
      body
      scene.cast.add(group);
      enemiesShadow.push(group); = true
    };

    for (let i
      body =.userData.part 0; i = "body < 5;";
      body i.user++) spawnEnemyData.target(i =);

    let yaw group; =
      group 0;.add
    let pitch(body =); 0;

      const head =
    let new health THREE = 100.Mesh;(new
    let score = THREE 0.Sphere;Geometry(
    let0. is28,Locked 16 = false,; 12), head
    let isMaterialDead); = false;
      head
    const keys.position.y = = new Set(); 1.
    const raycaster62 =; new THREE
      head.Ray.castcasterShadow(); = true
    const; clock
      head.user = new THREEData.Clock.part = "();head

    const health";El =
      head document.user.getElementByIdData("health.target = group;");
    const scoreEl = document
      group.get.add(headElementById(");

      constscore");
    const base = remainingEl = new THREE document.Mesh.getElementBy(Id("
        newremaining"); THREE
    const message.CylinderElGeometry =(0. document.getElementById("55,message"); 0
    const. hitEl = document.getElementBy55Id(", 0.hit");12,

    function 16 updateHud() {),
        new
      healthEl THREE.textContent =.MeshStandardMaterial health;({ color:
      scoreEl.textContent = 0 scorex;29343
      remainingElb.textContent = }) enemies
      );.filter(enemy
      base => enemy.position.user.y =Data.alive 0)..length;
    }06;
      base

    function startGame.cast() {
      if (!isShadowDead = true;)
      group.add renderer(base);.domElement.request

      scenePointer.addLock();
    }(group);
      targets

    renderer.dom.pushElement.addEventListener(group(");click", ()
      updateHUD(); => {
    }
      if (!isLocked

    for) { (let i =
        startGame(); 0; i
      } else if < max (!Targets; i++)is spawnDead) {Target();
        shoot();

    function
      }
    }); showHitmark()

    document.addEventListener {("
      hitmarkpointer.stylelockchange.opacity = "", ()1 =>"; {
      hit
      isLocked =mark documentTimer.pointer =LockElement === 0 renderer.16.dom;Element;
    }
      message

    function shoot() {El
      const.class now = performanceList.now();
      if (.togglenow - last("Shot < 180hidden", isLocked || isDead); ||
    });

    document.add reloadEventTimer >Listener(" 0mousemove)", event return; => {
      if
      last (!isShotLocked = now; ||

      if ( isDeadammo) return;
      yaw -= event.m <=ovementX * 0) { 0.0022;
      pitch -=
        reload event();.movementY * 0
        return.002;2;
      }
      pitch = Math.max(-1.35, Math.min(1
      ammo.--;35, pitch));
      updateHUD();
    });

    document.add

      raycaster.setEventListener("Fromkeydown",Camera event => {(new THREE
      keys.Vector2.add((event0,.code);
      if 0 (), camera);event.code === "
      constSpace target") eventParts = targets.prevent.flatDefault();Map
    });(target => target.children);
    document.addEventListener("
      constkeyup", event intersections => keys = raycaster.delete.inter(event.code));sectObjects(targetParts

    let shotCooldown, = false); 0;

      if (
    functioninter shoot() {sections
      if (.length >shotCooldown 0 >) 0 {) return;
        const hit =
      shot intersectionsCooldown =[0]. 0.object;22
        const target = hit;

      camera.rotation.set.userData.target;(pitch, yaw, 0,
        show "Hitmark();YXZ");
      ray

        constcaster.set damage = hitFrom.userData.partCamera ===(new THREE ".Vector2head" ?(0, 0), 100 camera :); 34;
        target.userData.health = (target.userData
      const targets =.health enemies
        .filter(enemy => enemy.user ??Data.al 100ive))
        . -flatMap damage;(enemy =>

        if enemy (.childrentarget);.user
      const hitData =.health raycaster <=.inter 0) {sectObjects(targets
          score, += false)[ hit.userData.part0 ===]; "

      if (head"hit && ? hit.distance < 150 45) { : 100
        const enemy =; hit
          scene.remove(target.object.userData);.enemy;
        enemy.user
          constData index =.health -= targets.index 50Of(target);;
        hitEl
          if (index.style !==.opacity = "1"; -
        set1) targetsTimeout(() => hit.splice(index,El.style.opacity = 1 "0);",
          updateHUD 100(););
          set
        if (enemyTimeout.userData.health <=(() => { 0) {
            if
          enemy (targets.user.lengthData.alive = false; < maxTargets) spawnTarget();
          scene
          },.remove(enemy);
          score++; 900
          update);
        }Hud();
          if
      } (
    }enemies.every

    function(item reload() => !item {
      if.user (Data.alivereload)) {Timer
            message >El 0 || ammo ===.innerHTML = "훈 12) return련; 완료
      reloadTimer! = 1<br. /><small>1클;릭해서 다시
      ammoElement 시작.text</smallContent = ">";
            message재El.classList장.remove("전 중…";hidden");
            document.exit
      setPointerTimeout(()Lock(); => {
          }
        }
      }
        ammo = 12
    };
        reloadTimer

    function = resetGame() 0; {
      health =
        updateHUD 100();;
      is
      }, 110Dead = false;0);
    }
      camera.position.set(0, 1.

    document7,.getElementBy 8 width: 16px; height: 2px; top: 7px; }
    #hitmarker {
      position: fixed;
      left: 50%;
      top: calc(50% + 22px);
      transform: translateX(-50%);
      color: #ff6464;
      font-size: 14px;
      font-weight: 800;
      opacity: 0;
      pointer-events: none;
    }
  </style>
</head>
<body>
  <div id="hud">
    <div>전술 FPS 프로토타입</div>
    <div>명중 <span id="score">0</span> · 탄약 ∞</div>
  </div>
  <div id="crosshair"></div>
  <div id="hitmarker">명중!</div>
  <div id="message">
    <strong>훈련장</strong><br>
    WASD 이동 · 마우스 조준 · 클릭 사격 · ESC 일시정지<br>
    <button id="startButton">게임 시작</button>
  </div>

  <script type="module">
    import * as THREE from "https://[Log in to view URL]";

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x9cc7d3);
    scene.fog = new THREE.Fog(0x9cc7d3, 25, 75);

    const camera = new THREE.PerspectiveCamera(
      75, window.innerWidth / window.innerHeight, 0.1, 100
    );
    camera.position.set(0, 1.65, 7);
    camera.rotation.order = "YXZ";

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    document.body.appendChild(renderer.domElement);

    scene.add(new THREE.HemisphereLight(0xe7f7ff, 0x47505c, 2));
    const sun = new THREE.DirectionalLight(0xffffff, 2.2);
    sun.position.set(-8, 14, 6);
    sun.castShadow = true;
    scene.add(sun);

    const materials = {
      floor: new THREE.MeshStandardMaterial({ color: 0x59635c, roughness: 0.95 }),
      wall: new THREE.MeshStandardMaterial({ color: 0x7d8989, roughness: 0.85 }),
      accent: new THREE.MeshStandardMaterial({ color: 0x263640, roughness: 0.7 }),
      target: new THREE.MeshStandardMaterial({
        color: 0xf05b67, roughness: 0.65, emissive: 0x3b080d
      }),
      head: new THREE.MeshStandardMaterial({
        color: 0xffb26b, roughness: 0.65, emissive: 0x331507
      })
    };

    function addBox(x, y, z, sx, sy, sz, material, castShadow = true) {
      const mesh = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz), material);
      mesh.position.set(x, y, z);
      mesh.castShadow = castShadow;
      mesh.receiveShadow = true;
      scene.add(mesh);
      return mesh;
    }

    const floor = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), materials.floor);
    floor.rotation.x = -Math.PI / 2;
    floor.receiveShadow = true;
    scene.add(floor);

    addBox(0, 2, -19, 30, 4, 1, materials.wall);
    addBox(-15, 2, -8, 1, 4, 22, materials.wall);
    addBox(15, 2, -8, 1, 4, 22, materials.wall);
    addBox(-6, 1, -5, 3, 2, 2, materials.accent);
    addBox(6, 1.5, -10, 4, 3, 2, materials.accent);
    addBox(0, 0.65, -14, 5, 1.3, 1.5, materials.accent);

    const targets = [];
    const targetMeshes = [];
    const targetPositions = [
      [-10, -12], [-5, -15], [0, -11], [5, -16], [10, -12]
    ];

    for (const [x, z] of targetPositions) {
      const group = new THREE.Group();
      group.position.set(x, 0, z);

      const body = new THREE.Mesh(
        new THREE.BoxGeometry(0.72, 1.05, 0.42), materials.target
      );
      body.position.y = 1.1;
      body.castShadow = true;

      const head = new THREE.Mesh(
        new THREE.SphereGeometry(0.28, 18, 14), materials.head
      );
      head.position.y = 1.85;
      head.castShadow = true;

      group.add(body, head);
      scene.add(group);

      const target = { group, parts: [body, head], active: true, respawnAt: 0 };
      body.userData.target = target;
      head.userData.target = target;
      targets.push(target);
      targetMeshes.push(body, head);
    }

    const keys = new Set();
    const raycaster = new THREE.Raycaster();
    const message = document.querySelector("#message");
    const scoreDisplay = document.querySelector("#score");
    const hitmarker = document.querySelector("#hitmarker");
    let score = 0;
    let yaw = 0;
    let pitch = 0;
    let lastShot = 0;
    let hitmarkerUntil = 0;

    document.querySelector("#startButton").addEventListener("click", () => {
      renderer.domElement.requestPointerLock();
    });

    document.addEventListener("pointerlockchange", () => {
      message.style.display = document.pointerLockElement ? "none" : "block";
    });

    document.addEventListener("mousemove", (event) => {
      if (document.pointerLockElement !== renderer.domElement) return;
      yaw -= event.movementX * 0.0022;
      pitch -= event.movementY * 0.0022;
      pitch = THREE.MathUtils.clamp(pitch, -1.35, 1.35);
      camera.rotation.set(pitch, yaw, 0);
    });

    document.addEventListener("keydown", (event) => keys.add(event.code));
    document.addEventListener("keyup", (event) => keys.delete(event.code));

    document.addEventListener("mousedown", (event) => {
      if (event.button !== 0 || document.pointerLockElement !== renderer.domElement) return;

      const now = performance.now();
      if (now - lastShot < 160) return;
      lastShot = now;

      raycaster.setFromCamera(new THREE.Vector2(0, 0), camera);
      const hit = raycaster.intersectObjects(targetMeshes, false)[0];
      if (!hit) return;

      const target = hit.object.userData.target;
      if (!target?.active) return;

      target.active = false;
      target.group.visible = false;
      target.respawnAt = now + 1800;
      scoreDisplay.textContent = String(++score);
      hitmarkerUntil = now + 220;
    });

    let previousTime = performance.now();

    function animate(now) {
      requestAnimationFrame(animate);
      const delta = Math.min((now - previousTime) / 1000, 0.05);
      previousTime = now;

      if (document.pointerLockElement === renderer.domElement) {
        const forward = new THREE.Vector3(-Math.sin(yaw), 0, -Math.cos(yaw));
        const right = new THREE.Vector3(Math.cos(yaw), 0, -Math.sin(yaw));
        const movement = new THREE.Vector3();

        if (keys.has("KeyW")) movement.add(forward);
        if (keys.has("KeyS")) movement.sub(forward);
        if (keys.has("KeyD")) movement.add(right);
        if (keys.has("KeyA")) movement.sub(right);

        if (movement.lengthSq() > 0) {
          movement.normalize().multiplyScalar(5 * delta);
          camera.position.add(movement);
        }

        camera.position.x = THREE.MathUtils.clamp(camera.position.x, -13.5, 13.5);
        camera.position.z = THREE.MathUtils.clamp(camera.position.z, -17, 9);
      }

      for (const target of targets) {
        if (!target.active && now >= target.respawnAt) {
          target.active = true;
          target.group.visible = true;
        }
      }

      hitmarker.style.opacity = now < hitmarkerUntil ? "1" : "0";
      renderer.render(scene, camera);
    }

    requestAnimationFrame(animate);

    window.addEventListener("resize", () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>
```);Id("start
      score-button"). = 0add;EventListener("click
      for", ( () =>const enemy of {
      renderer enemies) scene

Embed on website

To embed this project on your website, copy the following code and paste it into your website's HTML: