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
To embed this project on your website, copy the following code and paste it into your website's HTML: