FPS / app /index.html
ftrtemis's picture
Update app/index.html
b3656ed verified
Raw
History Blame Contribute Delete
49.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Not Halo: Full Stack</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; touch-action: none; -webkit-touch-callout: none; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui; user-select: none; }
canvas { display: block; }
#ui { position: absolute; inset: 0; pointer-events: none; display: none; flex-direction: column; justify-content: space-between; padding: 15px; z-index: 10; }
#crosshair { position: absolute; top: 50%; left: 50%; width: 24px; height: 24px; transform: translate(-50%,-50%); pointer-events: none; z-index: 12; }
#crosshair::before, #crosshair::after { content:''; position:absolute; background:rgba(255,255,255,0.9); box-shadow:0 0 4px #000; }
#crosshair::before { top:11px; left:0; width:24px; height:2px; }
#crosshair::after { top:0; left:11px; width:2px; height:24px; }
#scope { position:absolute; inset:0; display:none; z-index:11; pointer-events:none; }
#scope .ring { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:300px; height:300px; border:2px solid rgba(0,255,100,0.6); border-radius:50%; box-shadow:0 0 0 9999px rgba(0,10,0,0.85); }
#scope .cross { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:2px; height:60px; background:rgba(0,255,100,0.8); }
#scope .cross::after { content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:60px; height:2px; background:rgba(0,255,100,0.8); }
#hud-top { display:flex; justify-content:space-between; align-items:flex-start; }
#stats { background:rgba(0,0,0,0.5); padding:10px 14px; border-radius:8px; color:#fff; font-size:13px; line-height:1.6; border:1px solid rgba(255,255,255,0.08); backdrop-filter:blur(4px); }
#stats span { color:#00ffaa; font-weight:700; }
#weapon-info { margin-top:6px; color:#ffcc00; font-size:11px; letter-spacing:1px; text-transform:uppercase; }
#health-bar { width:180px; height:18px; background:rgba(0,0,0,0.5); border-radius:9px; overflow:hidden; border:1px solid rgba(255,255,255,0.15); margin-top:6px; }
#health-fill { width:100%; height:100%; background:linear-gradient(90deg,#ff2244,#ff8844); transition:width .2s; }
#health-text { color:#fff; font-size:11px; text-align:center; margin-top:3px; text-shadow:0 0 4px rgba(0,0,0,0.9); }
#minimap { position:absolute; top:15px; right:15px; width:140px; height:140px; background:rgba(0,0,0,0.5); border-radius:50%; border:2px solid rgba(255,255,255,0.15); overflow:hidden; backdrop-filter:blur(2px); }
#minimap-canvas { width:100%; height:100%; border-radius:50%; }
#ammo-counter { position:absolute; bottom:20px; right:20px; color:#fff; text-align:right; text-shadow:0 0 8px rgba(0,0,0,0.9); }
#ammo-current { font-size:42px; font-weight:800; line-height:1; }
#ammo-total { font-size:14px; opacity:.7; }
#reload-bar { width:80px; height:4px; background:rgba(255,255,255,0.2); border-radius:2px; margin-top:4px; margin-left:auto; overflow:hidden; display:none; }
#reload-fill { width:0%; height:100%; background:#ffcc00; transition:width .1s linear; }
#weapon-model { position:absolute; bottom:-20px; left:50%; transform:translateX(-50%); width:120px; height:220px; z-index:8; pointer-events:none; perspective:400px; }
.gun-body { position:absolute; bottom:0; left:50%; transform:translateX(-50%) rotateX(-5deg); width:20px; height:160px; background:linear-gradient(90deg,#222,#444,#222); border-radius:6px 6px 0 0; transform-origin:bottom center; transition:transform .1s; }
.gun-barrel { position:absolute; top:-40px; left:50%; transform:translateX(-50%); width:10px; height:50px; background:#333; border-radius:4px; }
.gun-mag { position:absolute; bottom:20px; left:-8px; width:14px; height:50px; background:#2a2a2a; transform:rotate(10deg); border-radius:2px; }
.recoil { animation:recoil .12s ease-out; }
@keyframes recoil { 0%{transform:translateX(-50%) rotateX(-5deg) translateY(0);} 50%{transform:translateX(-50%) rotateX(15deg) translateY(20px);} 100%{transform:translateX(-50%) rotateX(-5deg) translateY(0);} }
#damage-overlay { position:absolute; inset:0; background:rgba(255,0,0,0); pointer-events:none; z-index:5; transition:background .15s; }
.damage-text { position:absolute; color:#ff4444; font-weight:900; font-size:22px; pointer-events:none; z-index:15; animation:damageFloat .9s forwards; text-shadow:0 0 4px #000; }
@keyframes damageFloat { 0%{opacity:1;transform:translateY(0) scale(1);} 100%{opacity:0;transform:translateY(-60px) scale(1.4);} }
#kill-feed { position:absolute; top:70px; right:15px; display:flex; flex-direction:column; gap:4px; align-items:flex-end; pointer-events:none; }
.kill-msg { background:rgba(0,0,0,0.5); color:#fff; padding:4px 10px; border-radius:4px; font-size:12px; animation:killSlide 4s forwards; border-left:2px solid #ffcc00; }
@keyframes killSlide { 0%{opacity:1;transform:translateX(0);} 80%{opacity:1;} 100%{opacity:0;transform:translateX(20px);} }
#start-screen { position:absolute; inset:0; background:linear-gradient(135deg,#050510 0%,#101025 50%,#081018 100%); display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:20; pointer-events:auto; cursor:default; }
#start-screen h1 { font-size:52px; color:#fff; text-transform:uppercase; letter-spacing:6px; text-shadow:0 0 30px rgba(0,200,255,0.4); background:linear-gradient(180deg,#fff 0%,#88ccff 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; text-align:center; }
#start-screen .subtitle { color:#88aacc; font-size:16px; margin:8px 0 30px; letter-spacing:2px; text-align:center; }
#start-btn { padding:16px 48px; font-size:18px; color:#fff; background:linear-gradient(135deg,#0066ff,#00ccff); border:none; border-radius:50px; cursor:pointer; text-transform:uppercase; letter-spacing:3px; font-weight:700; box-shadow:0 0 30px rgba(0,150,255,0.4); transition:transform .2s,box-shadow .2s; }
#start-btn:hover { transform:scale(1.05); box-shadow:0 0 40px rgba(0,200,255,0.6); }
.controls-info { margin-top:25px; color:#6688aa; font-size:13px; text-align:center; line-height:2.2; max-width:90vw; }
.controls-info kbd { background:rgba(255,255,255,0.1); padding:3px 10px; border-radius:4px; border:1px solid rgba(255,255,255,0.15); color:#aaccff; font-family:monospace; }
#mobile-ui { display:none; position:absolute; inset:0; z-index:14; pointer-events:none; }
.touch-zone { position:absolute; pointer-events:auto; }
#stick-zone { left:0; bottom:0; width:50%; height:60%; }
#look-zone { right:0; bottom:0; width:50%; height:60%; }
#stick-knob { position:absolute; width:70px; height:70px; border-radius:50%; background:rgba(255,255,255,0.15); border:2px solid rgba(255,255,255,0.3); transform:translate(-50%,-50%); display:none; pointer-events:none; }
#stick-center { position:absolute; width:40px; height:40px; border-radius:50%; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); transform:translate(-50%,-50%); display:none; pointer-events:none; }
#btn-fire { position:absolute; right:30px; bottom:140px; width:70px; height:70px; border-radius:50%; background:rgba(255,50,50,0.25); border:2px solid rgba(255,100,100,0.5); color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; font-size:12px; letter-spacing:1px; }
#btn-jump { position:absolute; right:30px; bottom:230px; width:50px; height:50px; border-radius:50%; background:rgba(255,255,255,0.15); border:2px solid rgba(255,255,255,0.3); color:#fff; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; }
#btn-weapon { position:absolute; right:110px; bottom:140px; width:50px; height:50px; border-radius:50%; background:rgba(0,200,255,0.2); border:2px solid rgba(0,200,255,0.4); color:#fff; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; }
#connection-status { position:absolute; top:15px; left:50%; transform:translateX(-50%); background:rgba(0,0,0,0.5); padding:4px 12px; border-radius:12px; font-size:11px; color:#888; z-index:16; pointer-events:none; }
.conn-online { color:#00ffaa !important; }
.conn-offline { color:#ff4444 !important; }
</style>
<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>
</head>
<body>
<div id="ui">
<div id="hud-top">
<div>
<div id="stats">
<div>Score: <span id="score">0</span></div>
<div>Kills: <span id="kills">0</span></div>
<div>Time: <span id="time">0:00</span></div>
<div id="weapon-info">AR-15 [1]</div>
</div>
<div style="margin-top:6px;">
<div id="health-bar"><div id="health-fill"></div></div>
<div id="health-text">HEALTH 100%</div>
</div>
</div>
<div id="minimap"><canvas id="minimap-canvas" width="140" height="140"></canvas></div>
</div>
<div id="crosshair"></div>
<div id="scope"><div class="ring"></div><div class="cross"></div></div>
<div id="damage-overlay"></div>
<div id="kill-feed"></div>
<div id="ammo-counter">
<div id="ammo-current">30</div>
<div id="ammo-total"><span id="ammo-reserve">120</span> reserve</div>
<div id="reload-bar"><div id="reload-fill"></div></div>
</div>
<div id="weapon-model"><div class="gun-body" id="gun"><div class="gun-barrel"></div><div class="gun-mag"></div></div></div>
<div id="connection-status">Single Player</div>
</div>
<div id="mobile-ui">
<div class="touch-zone" id="stick-zone"></div>
<div class="touch-zone" id="look-zone"></div>
<div id="stick-center"></div>
<div id="stick-knob"></div>
<div class="touch-zone" id="btn-fire">FIRE</div>
<div class="touch-zone" id="btn-jump">JUMP</div>
<div class="touch-zone" id="btn-weapon">WEP</div>
</div>
<div id="start-screen">
<h1>Neon Strike</h1>
<div class="subtitle">FPS • Multiplayer • Procedural • Mobile</div>
<button id="start-btn">Enter Combat</button>
<div class="controls-info" id="controls-desktop">
<kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> Weapons •
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> Move •
<kbd>SPACE</kbd> Jump •
<kbd>CLICK</kbd> Shoot •
<kbd>R</kbd> Reload •
<kbd>RIGHT CLICK</kbd> Scope •
<kbd>SHIFT</kbd> Sprint
</div>
<div class="controls-info" id="controls-mobile" style="display:none;">
Left side: Virtual Joystick • Right side: Drag to Look<br>
Red Button: Shoot • Blue Button: Switch Weapon • White Button: Jump
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
const CONFIG = {
wsUrl: (() => {
const isHF = location.hostname.includes('hf.space');
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
return isHF ? `${protocol}//${location.host}/ws` : 'ws://localhost:7860/ws';
})(),
isMobile: /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()),
mapSize: 40,
cellSize: 5,
gravity: 28,
playerHeight: 1.7,
walkSpeed: 5.5,
sprintSpeed: 9,
jumpForce: 10,
botCount: 4
};
class SoundSys {
constructor() {
this.ctx = null;
this.master = null;
this.initialized = false;
}
init() {
if (this.initialized) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.master = this.ctx.createGain();
this.master.gain.value = 0.4;
this.master.connect(this.ctx.destination);
this.initialized = true;
}
_noise(duration, freq1, type = 'bandpass', vol = 1) {
if (!this.ctx) return;
const t = this.ctx.currentTime;
const bufferSize = this.ctx.sampleRate * duration;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
const src = this.ctx.createBufferSource();
src.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = type;
filter.frequency.value = freq1;
filter.Q.value = 1;
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(vol, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + duration);
src.connect(filter).connect(gain).connect(this.master);
src.start(t);
}
shoot(w) {
if (w.name === 'Shotgun') this._noise(0.12, 300, 'lowpass', 1.2);
else if (w.name === 'Sniper') {
this._noise(0.15, 2000, 'highpass', 0.8);
this._noise(0.2, 400, 'lowpass', 0.6);
} else {
this._noise(0.08, 1200, 'bandpass', 0.7);
}
}
hit() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const g = this.ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(1200, this.ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, this.ctx.currentTime + 0.1);
g.gain.setValueAtTime(0.2, this.ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.1);
osc.connect(g).connect(this.master);
osc.start();
osc.stop(this.ctx.currentTime + 0.1);
}
reload() {
if (!this.ctx) return;
const t = this.ctx.currentTime;
[0, 0.15, 0.3].forEach((d) => {
const osc = this.ctx.createOscillator();
const g = this.ctx.createGain();
osc.type = 'square';
osc.frequency.value = 200 + Math.random() * 100;
g.gain.setValueAtTime(0.05, t + d);
g.gain.exponentialRampToValueAtTime(0.001, t + d + 0.08);
osc.connect(g).connect(this.master);
osc.start(t + d);
osc.stop(t + d + 0.08);
});
}
explosion() {
this._noise(0.4, 200, 'lowpass', 1.5);
}
step() {
if (!this.ctx || Math.random() > 0.3) return;
this._noise(0.04, 600, 'bandpass', 0.08);
}
ambient() {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const g = this.ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 50;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 120;
filter.Q.value = 5;
g.gain.value = 0.015;
osc.connect(filter).connect(g).connect(this.master);
osc.start();
}
}
const audio = new SoundSys();
const WEAPONS = [
{ id: 0, name: 'AR-15', damage: 28, fireRate: 110, ammo: 30, reserve: 120, spread: 0.025, auto: true, reload: 1900, pellets: 1, zoomFOV: 75 },
{ id: 1, name: 'Shotgun', damage: 16, fireRate: 900, ammo: 8, reserve: 32, spread: 0.18, auto: false, reload: 2800, pellets: 7, zoomFOV: 75 },
{ id: 2, name: 'Sniper', damage: 100, fireRate: 1300, ammo: 5, reserve: 20, spread: 0.002, auto: false, reload: 3500, pellets: 1, zoomFOV: 25 }
];
let currentWeapon = 0;
let isScoped = false;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.FogExp2(0x050510, 0.018);
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 300);
camera.position.y = CONFIG.playerHeight;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0x202040, 0.5));
const dirLight = new THREE.DirectionalLight(0x8899ff, 0.5);
dirLight.position.set(30, 60, 20);
dirLight.castShadow = true;
dirLight.shadow.camera.left = -60;
dirLight.shadow.camera.right = 60;
dirLight.shadow.camera.top = 60;
dirLight.shadow.camera.bottom = -60;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
scene.add(dirLight);
class LevelGen {
constructor(size, cell) {
this.size = size;
this.cell = cell;
this.grid = Array(size).fill().map(() => Array(size).fill(1));
this.walls = [];
this.neons = [];
this.generate();
}
generate() {
const cx = Math.floor(this.size / 2);
const cy = Math.floor(this.size / 2);
let x = cx;
let y = cy;
this.grid[y][x] = 0;
for (let i = 0; i < 500; i++) {
const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const d = dirs[Math.floor(Math.random() * 4)];
x += d[0];
y += d[1];
x = Math.max(1, Math.min(this.size - 2, x));
y = Math.max(1, Math.min(this.size - 2, y));
this.grid[y][x] = 0;
if (Math.random() < 0.08) {
for (let ry = -1; ry <= 1; ry++) {
for (let rx = -1; rx <= 1; rx++) {
if (y + ry > 0 && y + ry < this.size - 1 && x + rx > 0 && x + rx < this.size - 1) {
this.grid[y + ry][x + rx] = 0;
}
}
}
}
}
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const sy = cy + dy;
const sx = x + dx;
if (sy >= 0 && sy < this.size && sx >= 0 && sx < this.size) this.grid[sy][sx] = 0;
}
}
}
build() {
const wallMat = new THREE.MeshStandardMaterial({ color: 0x2a2a3e, roughness: 0.9 });
const floorGeo = new THREE.PlaneGeometry(this.size * this.cell, this.size * this.cell);
const floorMat = new THREE.MeshStandardMaterial({ color: 0x111122, roughness: 0.9, metalness: 0.2 });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
const grid = new THREE.GridHelper(this.size * this.cell, this.size, 0x334466, 0x223344);
grid.position.y = 0.01;
scene.add(grid);
let wallCount = 0;
for (let y = 0; y < this.size; y++) for (let x = 0; x < this.size; x++) if (this.grid[y][x] === 1) wallCount++;
const wGeo = new THREE.BoxGeometry(this.cell, 6, this.cell);
const iMesh = new THREE.InstancedMesh(wGeo, wallMat, wallCount);
iMesh.castShadow = true;
iMesh.receiveShadow = true;
let idx = 0;
const dummy = new THREE.Object3D();
for (let y = 0; y < this.size; y++) {
for (let x = 0; x < this.size; x++) {
if (this.grid[y][x] === 1) {
dummy.position.set((x - this.size / 2) * this.cell, 3, (y - this.size / 2) * this.cell);
dummy.updateMatrix();
iMesh.setMatrixAt(idx++, dummy.matrix);
}
}
}
scene.add(iMesh);
const neonColors = [0x00ffff, 0xff00ff, 0xff6600, 0x00ff66, 0x6644ff];
let placed = 0;
while (placed < 8) {
const rx = Math.floor(Math.random() * this.size);
const ry = Math.floor(Math.random() * this.size);
if (this.grid[ry][rx] === 0 && Math.abs(rx - this.size / 2) > 4 && Math.abs(ry - this.size / 2) > 4) {
const cx = (rx - this.size / 2) * this.cell;
const cz = (ry - this.size / 2) * this.cell;
const colGeo = new THREE.CylinderGeometry(0.4, 0.4, 7, 8);
const ccol = neonColors[placed % neonColors.length];
const colMat = new THREE.MeshStandardMaterial({ color: ccol, emissive: ccol, emissiveIntensity: 2.5, roughness: 0.2, metalness: 0.8 });
const col = new THREE.Mesh(colGeo, colMat);
col.position.set(cx, 3.5, cz);
col.castShadow = true;
scene.add(col);
const pl = new THREE.PointLight(ccol, 2.5, 18);
pl.position.set(cx, 3.5, cz);
scene.add(pl);
this.neons.push({ mesh: col, box: new THREE.Box3().setFromObject(col) });
placed++;
}
}
for (let i = 0; i < 20; i++) {
const rx = Math.floor(Math.random() * this.size);
const ry = Math.floor(Math.random() * this.size);
if (this.grid[ry][rx] === 0 && (Math.abs(rx - this.size / 2) > 2 || Math.abs(ry - this.size / 2) > 2)) {
const s = 1.5 + Math.random() * 2;
const cx = (rx - this.size / 2) * this.cell;
const cz = (ry - this.size / 2) * this.cell;
const cm = new THREE.Mesh(new THREE.BoxGeometry(s, s, s), new THREE.MeshStandardMaterial({ color: 0x4a3728 }));
cm.position.set(cx, s / 2, cz);
cm.castShadow = true;
cm.receiveShadow = true;
scene.add(cm);
this.neons.push({ mesh: cm, box: new THREE.Box3().setFromObject(cm) });
}
}
for (let y = 0; y < this.size; y++) {
for (let x = 0; x < this.size; x++) {
if (this.grid[y][x] === 1) {
const b = new THREE.Box3();
const px = (x - this.size / 2) * this.cell;
const pz = (y - this.size / 2) * this.cell;
b.min.set(px - this.cell / 2, 0, pz - this.cell / 2);
b.max.set(px + this.cell / 2, 6, pz + this.cell / 2);
this.walls.push(b);
}
}
}
this.neons.forEach((n) => this.walls.push(n.box));
}
}
const level = new LevelGen(CONFIG.mapSize, CONFIG.cellSize);
level.build();
const keys = { w: false, a: false, s: false, d: false, shift: false };
let velocity = new THREE.Vector3();
let isGrounded = true;
const pBox = new THREE.Box3();
const pSize = new THREE.Vector3(0.5, CONFIG.playerHeight, 0.5);
function movePlayer(dt) {
const w = WEAPONS[currentWeapon];
const speed = (keys.shift && !isScoped) ? CONFIG.sprintSpeed : CONFIG.walkSpeed;
camera.fov = THREE.MathUtils.lerp(camera.fov, isScoped ? w.zoomFOV : 75, dt * 8);
camera.updateProjectionMatrix();
velocity.x -= velocity.x * 10 * dt;
velocity.z -= velocity.z * 10 * dt;
velocity.y -= CONFIG.gravity * dt;
let dz = (keys.w ? 1 : 0) - (keys.s ? 1 : 0);
let dx = (keys.d ? 1 : 0) - (keys.a ? 1 : 0);
if (mobileInput.move.lengthSq() > 0) {
dx = mobileInput.move.x;
dz = -mobileInput.move.y;
}
const len = Math.hypot(dx, dz);
if (len > 0) {
dx /= len;
dz /= len;
}
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3();
right.crossVectors(forward, new THREE.Vector3(0, 1, 0));
if (dz !== 0) velocity.addScaledVector(forward, dz * speed * 10 * dt);
if (dx !== 0) velocity.addScaledVector(right, dx * speed * 10 * dt);
controls.moveRight(-velocity.x * dt);
controls.moveForward(-velocity.z * dt);
camera.position.y += velocity.y * dt;
if (camera.position.y < CONFIG.playerHeight) {
velocity.y = 0;
camera.position.y = CONFIG.playerHeight;
isGrounded = true;
} else {
isGrounded = false;
}
pBox.setFromCenterAndSize(camera.position, pSize);
for (const b of level.walls) {
if (pBox.intersectsBox(b)) {
const center = new THREE.Vector3();
b.getCenter(center);
const push = new THREE.Vector3().subVectors(camera.position, center);
push.y = 0;
push.normalize();
camera.position.x += push.x * 0.15;
camera.position.z += push.z * 0.15;
}
}
const worldBound = (CONFIG.mapSize * CONFIG.cellSize) / 2 - 1;
camera.position.x = Math.max(-worldBound, Math.min(worldBound, camera.position.x));
camera.position.z = Math.max(-worldBound, Math.min(worldBound, camera.position.z));
}
class MPManager {
constructor(url) {
this.url = url;
this.ws = null;
this.id = null;
this.remotePlayers = {};
this.connected = false;
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.connected = true;
updateConnStatus('Online', true);
audio.ambient();
};
this.ws.onmessage = (e) => this.handle(JSON.parse(e.data));
this.ws.onclose = () => {
this.connected = false;
updateConnStatus('Disconnected', false);
setTimeout(() => this.connect(), 3000);
};
this.ws.onerror = () => updateConnStatus('Offline', false);
} catch {
updateConnStatus('Offline', false);
}
}
handle(d) {
if (d.t === 'id') this.id = d.id;
if (d.t === 'state') {
d.players.forEach((p) => {
if (p.id === this.id) return;
this.updateRemote(p);
});
}
if (d.t === 'shoot') this.spawnRemoteBullet(d.origin || d.pos, d.dir, d.weapon);
if (d.t === 'kill') addKillFeed(d.killer, d.victim);
}
updateRemote(p) {
if (!this.remotePlayers[p.id]) {
const g = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.6, 0.4), new THREE.MeshStandardMaterial({ color: 0xff2244 }));
body.position.y = 0.8;
body.castShadow = true;
g.add(body);
const head = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.45, 0.45), new THREE.MeshStandardMaterial({ color: 0xffaa00 }));
head.position.y = 1.8;
g.add(head);
const nameTag = document.createElement('div');
nameTag.style.cssText = 'position:absolute;color:#fff;font-size:12px;font-weight:700;text-shadow:0 0 4px #000;pointer-events:none;transform:translate(-50%,-100%);';
nameTag.textContent = p.name || 'Enemy';
document.body.appendChild(nameTag);
scene.add(g);
this.remotePlayers[p.id] = {
mesh: g,
nametag: nameTag,
targetPos: new THREE.Vector3(...p.pos),
targetRot: new THREE.Quaternion(...p.rot)
};
}
const rp = this.remotePlayers[p.id];
rp.targetPos.set(...p.pos);
rp.targetRot.set(...p.rot);
}
spawnRemoteBullet(pos, dir, weaponId) {
if (!pos || !dir) return;
const w = WEAPONS[weaponId] || WEAPONS[0];
const b = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.05, 0.3), new THREE.MeshBasicMaterial({ color: w.name === 'Sniper' ? 0x66ccff : 0xffff00 }));
b.position.set(...pos);
b.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(...dir));
scene.add(b);
const vel = new THREE.Vector3(...dir).multiplyScalar(60);
const tick = () => {
b.position.addScaledVector(vel, 0.016);
if (b.position.length() > 200) {
scene.remove(b);
return;
}
requestAnimationFrame(tick);
};
tick();
}
sync(pos, rot) {
if (this.connected && this.ws.readyState === 1) {
this.ws.send(JSON.stringify({ t: 'pos', pos: [pos.x, pos.y, pos.z], rot: [rot.x, rot.y, rot.z, rot.w] }));
}
}
sendShoot(origin, dir, weaponId) {
if (this.connected && this.ws.readyState === 1) {
this.ws.send(JSON.stringify({ t: 'shoot', origin: [origin.x, origin.y, origin.z], dir: [dir.x, dir.y, dir.z], weapon: weaponId }));
}
}
update(dt) {
for (const id in this.remotePlayers) {
const rp = this.remotePlayers[id];
rp.mesh.position.lerp(rp.targetPos, dt * 10);
rp.mesh.quaternion.slerp(rp.targetRot, dt * 10);
const p = rp.mesh.position.clone().add(new THREE.Vector3(0, 2.2, 0)).project(camera);
const x = (p.x * 0.5 + 0.5) * innerWidth;
const y = (-p.y * 0.5 + 0.5) * innerHeight;
rp.nametag.style.left = `${x}px`;
rp.nametag.style.top = `${y}px`;
rp.nametag.style.display = p.z > 1 ? 'none' : 'block';
}
}
}
const mp = new MPManager(CONFIG.wsUrl);
class Bot {
constructor() {
this.group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xff2244, roughness: 0.3 });
this.body = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1.6, 0.4), bodyMat);
this.body.position.y = 0.8;
this.body.castShadow = true;
this.group.add(this.body);
this.head = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.45, 0.45), new THREE.MeshStandardMaterial({ color: 0xffaa00 }));
this.head.position.y = 1.8;
this.group.add(this.head);
this.group.position.set(999, 999, 999);
scene.add(this.group);
this.health = 100;
this.speed = 2.5 + Math.random() * 2;
this.dead = true;
this.lastAttack = 0;
this.flash = 0;
}
spawn() {
let attempts = 0;
let cx = 0;
let cz = 0;
do {
const rx = Math.floor(Math.random() * CONFIG.mapSize);
const ry = Math.floor(Math.random() * CONFIG.mapSize);
if (level.grid[ry][rx] === 0) {
cx = (rx - CONFIG.mapSize / 2) * CONFIG.cellSize;
cz = (ry - CONFIG.mapSize / 2) * CONFIG.cellSize;
if (new THREE.Vector3(cx, 0, cz).distanceTo(camera.position) > 20) break;
}
attempts++;
} while (attempts < 100);
this.group.position.set(cx, 0, cz);
this.health = 100 + Math.floor(Math.random() * 50);
this.dead = false;
}
update(dt, target) {
if (this.dead) return;
this.group.lookAt(target.x, this.group.position.y, target.z);
const dir = new THREE.Vector3().subVectors(target, this.group.position);
dir.y = 0;
const dist = dir.length();
dir.normalize();
if (dist > 1.5) {
this.group.position.x += dir.x * this.speed * dt;
this.group.position.z += dir.z * this.speed * dt;
}
if (dist < 2.5 && Date.now() - this.lastAttack > 1200) {
this.lastAttack = Date.now();
damagePlayer(12);
}
if (this.flash > 0) {
this.flash -= dt * 4;
this.body.material.emissive = new THREE.Color(0xffffff);
this.body.material.emissiveIntensity = this.flash;
} else {
this.body.material.emissive = new THREE.Color(0x000000);
this.body.material.emissiveIntensity = 0;
}
}
hit(dmg, pt) {
this.health -= dmg;
this.flash = 1;
showDamage(dmg, pt);
if (this.health <= 0) {
this.dead = true;
this.group.position.set(999, 999, 999);
state.kills += 1;
state.score += 100;
updateUI();
spawnParticles(pt, 0xff2244, 12);
audio.explosion();
setTimeout(() => this.spawn(), 2500);
}
}
}
const bots = Array.from({ length: CONFIG.botCount }, () => new Bot());
let particles = [];
function spawnParticles(pos, color, count) {
for (let i = 0; i < count; i++) {
const m = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.06, 0.06), new THREE.MeshBasicMaterial({ color }));
m.position.copy(pos).add(new THREE.Vector3((Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.4));
scene.add(m);
particles.push({ m, v: new THREE.Vector3((Math.random() - 0.5) * 8, Math.random() * 5, (Math.random() - 0.5) * 8), life: 1 });
}
}
const state = { score: 0, kills: 0, health: 100, startTime: 0, playing: false, canShoot: true, lastShot: 0, reloading: false };
function maxAmmoFor(weapon) {
if (weapon.name === 'AR-15') return 30;
if (weapon.name === 'Shotgun') return 8;
return 5;
}
function resetGame() {
state.score = 0;
state.kills = 0;
state.health = 100;
state.playing = false;
state.canShoot = true;
state.lastShot = 0;
state.reloading = false;
camera.position.set(0, CONFIG.playerHeight, 0);
camera.rotation.set(0, 0, 0);
velocity.set(0, 0, 0);
bots.forEach((b) => {
b.dead = true;
b.group.position.set(999, 999, 999);
setTimeout(() => b.spawn(), Math.random() * 2000);
});
WEAPONS.forEach((w) => {
w.ammo = maxAmmoFor(w);
w.reserve = w.name === 'AR-15' ? 120 : (w.name === 'Shotgun' ? 32 : 20);
});
currentWeapon = 0;
isScoped = false;
document.getElementById('scope').style.display = 'none';
document.getElementById('crosshair').style.display = 'block';
}
const controls = new PointerLockControls(camera, document.body);
const mobileInput = { move: new THREE.Vector2(), look: new THREE.Vector2() };
document.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
if (k === 'w') keys.w = true;
if (k === 'a') keys.a = true;
if (k === 's') keys.s = true;
if (k === 'd') keys.d = true;
if (k === ' ') { if (isGrounded) velocity.y = CONFIG.jumpForce; }
if (k === 'shift') keys.shift = true;
if (k === 'r') reloadWeapon();
if (k >= '1' && k <= '3') switchWeapon(parseInt(k, 10) - 1);
if (k === 'z') toggleScope();
});
document.addEventListener('keyup', (e) => {
const k = e.key.toLowerCase();
if (k === 'w') keys.w = false;
if (k === 'a') keys.a = false;
if (k === 's') keys.s = false;
if (k === 'd') keys.d = false;
if (k === 'shift') keys.shift = false;
});
document.addEventListener('mousedown', (e) => {
if (e.button === 2) {
e.preventDefault();
toggleScope();
}
if (state.playing && e.button === 0 && !CONFIG.isMobile) shoot();
});
document.addEventListener('wheel', (e) => {
if (!state.playing) return;
if (e.deltaY > 0) switchWeapon((currentWeapon + 1) % WEAPONS.length);
else switchWeapon((currentWeapon - 1 + WEAPONS.length) % WEAPONS.length);
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
if (CONFIG.isMobile) {
document.getElementById('controls-mobile').style.display = 'block';
document.getElementById('controls-desktop').style.display = 'none';
document.getElementById('mobile-ui').style.display = 'block';
const stickZone = document.getElementById('stick-zone');
const stickKnob = document.getElementById('stick-knob');
const stickCenter = document.getElementById('stick-center');
let stickId = null;
let stickOrigin = { x: 0, y: 0 };
stickZone.addEventListener('touchstart', (e) => {
const t = e.changedTouches[0];
stickId = t.identifier;
stickOrigin = { x: t.clientX, y: t.clientY };
stickKnob.style.display = 'block';
stickCenter.style.display = 'block';
stickKnob.style.left = `${t.clientX}px`;
stickCenter.style.left = `${t.clientX}px`;
stickKnob.style.top = `${t.clientY}px`;
stickCenter.style.top = `${t.clientY}px`;
}, { passive: false });
stickZone.addEventListener('touchmove', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const t = e.changedTouches[i];
if (t.identifier === stickId) {
const dx = t.clientX - stickOrigin.x;
const dy = t.clientY - stickOrigin.y;
const dist = Math.min(Math.hypot(dx, dy), 50);
const ang = Math.atan2(dy, dx);
mobileInput.move.set(Math.cos(ang) * dist / 50, Math.sin(ang) * dist / 50);
stickKnob.style.left = `${stickOrigin.x + Math.cos(ang) * dist}px`;
stickKnob.style.top = `${stickOrigin.y + Math.sin(ang) * dist}px`;
}
}
}, { passive: false });
const endStick = (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === stickId) {
stickId = null;
mobileInput.move.set(0, 0);
stickKnob.style.display = 'none';
stickCenter.style.display = 'none';
}
}
};
stickZone.addEventListener('touchend', endStick);
stickZone.addEventListener('touchcancel', endStick);
const lookZone = document.getElementById('look-zone');
const lookTouches = {};
lookZone.addEventListener('touchstart', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const t = e.changedTouches[i];
lookTouches[t.identifier] = { x: t.clientX, y: t.clientY };
}
}, { passive: false });
lookZone.addEventListener('touchmove', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) {
const t = e.changedTouches[i];
const prev = lookTouches[t.identifier];
if (prev) {
const sens = 0.004;
const dx = (t.clientX - prev.x) * sens;
const dy = (t.clientY - prev.y) * sens;
camera.rotation.y -= dx;
camera.rotation.x -= dy;
camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x));
lookTouches[t.identifier] = { x: t.clientX, y: t.clientY };
}
}
}, { passive: false });
lookZone.addEventListener('touchend', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) delete lookTouches[e.changedTouches[i].identifier];
});
lookZone.addEventListener('touchcancel', (e) => {
for (let i = 0; i < e.changedTouches.length; i++) delete lookTouches[e.changedTouches[i].identifier];
});
document.getElementById('btn-fire').addEventListener('touchstart', (e) => { e.preventDefault(); shoot(); }, { passive: false });
document.getElementById('btn-jump').addEventListener('touchstart', (e) => { e.preventDefault(); if (isGrounded) velocity.y = CONFIG.jumpForce; }, { passive: false });
document.getElementById('btn-weapon').addEventListener('touchstart', (e) => { e.preventDefault(); switchWeapon((currentWeapon + 1) % WEAPONS.length); }, { passive: false });
}
function reloadWeapon() {
const w = WEAPONS[currentWeapon];
const magMax = maxAmmoFor(w);
if (state.reloading || w.ammo >= magMax || w.reserve <= 0) return;
state.reloading = true;
state.canShoot = false;
audio.reload();
document.getElementById('reload-bar').style.display = 'block';
document.getElementById('reload-fill').style.transition = 'none';
document.getElementById('reload-fill').style.width = '0%';
setTimeout(() => {
document.getElementById('reload-fill').style.transition = `width ${Math.floor(w.reload * 0.8)}ms linear`;
document.getElementById('reload-fill').style.width = '100%';
}, 50);
setTimeout(() => {
const needed = magMax - w.ammo;
const taken = Math.min(needed, w.reserve);
w.ammo += taken;
w.reserve -= taken;
state.reloading = false;
state.canShoot = true;
document.getElementById('reload-bar').style.display = 'none';
updateUI();
}, w.reload);
}
function switchWeapon(idx) {
if (idx === currentWeapon) return;
currentWeapon = idx;
const w = WEAPONS[idx];
document.getElementById('weapon-info').textContent = `${w.name} [${idx + 1}]`;
const gun = document.getElementById('gun');
gun.style.filter = 'brightness(1.5)';
setTimeout(() => { gun.style.filter = 'none'; }, 150);
isScoped = false;
document.getElementById('scope').style.display = 'none';
document.getElementById('crosshair').style.display = 'block';
updateUI();
}
function toggleScope() {
if (currentWeapon !== 2) return;
isScoped = !isScoped;
document.getElementById('scope').style.display = isScoped ? 'block' : 'none';
document.getElementById('crosshair').style.display = isScoped ? 'none' : 'block';
}
function shoot() {
if (!state.playing || !state.canShoot || state.reloading) return;
const w = WEAPONS[currentWeapon];
if (w.ammo <= 0) {
reloadWeapon();
return;
}
if (Date.now() - state.lastShot < w.fireRate) return;
state.lastShot = Date.now();
w.ammo--;
updateUI();
audio.shoot(w);
const gun = document.getElementById('gun');
gun.classList.remove('recoil');
void gun.offsetWidth;
gun.classList.add('recoil');
const muzzlePos = camera.position.clone().add(new THREE.Vector3(0.25, -0.15, -0.4).applyQuaternion(camera.quaternion));
spawnParticles(muzzlePos, 0xffffaa, 4);
const rc = new THREE.Raycaster();
const pellets = w.pellets;
for (let i = 0; i < pellets; i++) {
const spread = isScoped ? 0 : w.spread;
const offset = new THREE.Vector3((Math.random() - 0.5) * spread, (Math.random() - 0.5) * spread, (Math.random() - 0.5) * spread);
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion).add(offset).normalize();
rc.set(camera.position, dir);
let hitSomething = false;
for (const b of bots) {
if (b.dead) continue;
const intersects = rc.intersectObjects([b.body, b.head]);
if (intersects.length > 0) {
const dmg = intersects[0].object === b.head ? w.damage * 2 : w.damage;
b.hit(dmg, intersects[0].point);
spawnParticles(intersects[0].point, 0xff0000, 6);
audio.hit();
hitSomething = true;
break;
}
}
if (!hitSomething) {
const wallMeshes = level.neons.map((n) => n.mesh);
const wi = rc.intersectObjects(wallMeshes);
if (wi.length > 0) spawnParticles(wi[0].point, 0xffaa00, 4);
}
}
const shootDir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
mp.sendShoot(camera.position, shootDir, currentWeapon);
if (w.ammo <= 0) reloadWeapon();
}
function damagePlayer(amt) {
state.health = Math.max(0, state.health - amt);
document.getElementById('damage-overlay').style.background = `rgba(255,0,0,${amt / 150})`;
setTimeout(() => { document.getElementById('damage-overlay').style.background = 'rgba(255,0,0,0)'; }, 120);
updateUI();
if (state.health <= 0) gameOver();
}
function showDamage(amt, pt) {
const el = document.createElement('div');
el.className = 'damage-text';
el.textContent = `-${amt}`;
document.body.appendChild(el);
const p = pt.clone().project(camera);
const x = (p.x * 0.5 + 0.5) * innerWidth;
const y = (-p.y * 0.5 + 0.5) * innerHeight;
el.style.left = `${x}px`;
el.style.top = `${y}px`;
setTimeout(() => el.remove(), 900);
}
function addKillFeed(killer, victim) {
const el = document.createElement('div');
el.className = 'kill-msg';
el.textContent = `${killer}${victim}`;
document.getElementById('kill-feed').appendChild(el);
setTimeout(() => el.remove(), 4000);
}
function updateConnStatus(text, ok) {
const el = document.getElementById('connection-status');
el.textContent = text;
el.classList.remove('conn-online', 'conn-offline');
el.classList.add(ok ? 'conn-online' : 'conn-offline');
}
function updateUI() {
document.getElementById('score').textContent = state.score;
document.getElementById('kills').textContent = state.kills;
const w = WEAPONS[currentWeapon];
document.getElementById('ammo-current').textContent = w.ammo;
document.getElementById('ammo-reserve').textContent = w.reserve;
document.getElementById('weapon-info').textContent = `${w.name} [${currentWeapon + 1}]`;
document.getElementById('health-fill').style.width = `${Math.max(0, state.health)}%`;
document.getElementById('health-text').textContent = `HEALTH ${Math.floor(Math.max(0, state.health))}%`;
const elapsed = Math.floor((Date.now() - state.startTime) / 1000);
const m = Math.floor(elapsed / 60);
const s = elapsed % 60;
document.getElementById('time').textContent = `${m}:${s.toString().padStart(2, '0')}`;
}
function gameOver() {
state.playing = false;
controls.unlock();
document.getElementById('ui').style.display = 'none';
const scr = document.getElementById('start-screen');
scr.style.display = 'flex';
scr.querySelector('h1').textContent = 'MISSION FAILED';
scr.querySelector('.subtitle').textContent = `Score ${state.score} • Kills ${state.kills}`;
document.getElementById('start-btn').textContent = 'Deploy Again';
resetGame();
}
const mmCanvas = document.getElementById('minimap-canvas');
const mmCtx = mmCanvas.getContext('2d');
function drawMinimap() {
mmCtx.fillStyle = '#050510';
mmCtx.fillRect(0, 0, 140, 140);
const scl = 140 / (CONFIG.mapSize * CONFIG.cellSize);
const ox = CONFIG.mapSize * CONFIG.cellSize / 2;
const oy = CONFIG.mapSize * CONFIG.cellSize / 2;
mmCtx.fillStyle = '#1a1a2e';
for (let y = 0; y < CONFIG.mapSize; y++) {
for (let x = 0; x < CONFIG.mapSize; x++) {
if (level.grid[y][x] === 1) {
mmCtx.fillRect((x * CONFIG.cellSize - ox) * scl + 70, (y * CONFIG.cellSize - oy) * scl + 70, CONFIG.cellSize * scl, CONFIG.cellSize * scl);
}
}
}
bots.forEach((b) => {
if (b.dead) return;
mmCtx.fillStyle = '#ff2244';
mmCtx.beginPath();
mmCtx.arc((b.group.position.x) * scl + 70, (b.group.position.z) * scl + 70, 2.5, 0, Math.PI * 2);
mmCtx.fill();
});
for (const id in mp.remotePlayers) {
const rp = mp.remotePlayers[id].mesh;
mmCtx.fillStyle = '#00ccff';
mmCtx.beginPath();
mmCtx.arc(rp.position.x * scl + 70, rp.position.z * scl + 70, 2.5, 0, Math.PI * 2);
mmCtx.fill();
}
mmCtx.fillStyle = '#00ffaa';
mmCtx.beginPath();
mmCtx.arc(camera.position.x * scl + 70, camera.position.z * scl + 70, 3, 0, Math.PI * 2);
mmCtx.fill();
const d = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
mmCtx.strokeStyle = '#00ffaa';
mmCtx.lineWidth = 1.5;
mmCtx.beginPath();
mmCtx.moveTo(camera.position.x * scl + 70, camera.position.z * scl + 70);
mmCtx.lineTo((camera.position.x + d.x * 6) * scl + 70, (camera.position.z + d.z * 6) * scl + 70);
mmCtx.stroke();
}
const clock = new THREE.Clock();
const startBtn = document.getElementById('start-btn');
const ui = document.getElementById('ui');
const startScreen = document.getElementById('start-screen');
function startGameSession() {
startScreen.style.display = 'none';
ui.style.display = 'flex';
state.playing = true;
state.startTime = Date.now();
state.health = 100;
bots.forEach((b) => b.spawn());
updateUI();
}
startBtn.addEventListener('click', () => {
audio.init();
mp.connect();
if (CONFIG.isMobile) {
startGameSession();
} else {
controls.lock();
}
});
controls.addEventListener('lock', () => {
startGameSession();
});
controls.addEventListener('unlock', () => {
if (state.health > 0 && !CONFIG.isMobile) {
startScreen.style.display = 'flex';
ui.style.display = 'none';
startBtn.textContent = 'Resume';
}
state.playing = false;
});
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
if (state.playing) {
movePlayer(dt);
bots.forEach((b) => b.update(dt, camera.position));
mp.update(dt);
particles = particles.filter((p) => {
p.life -= dt * 2;
p.m.position.addScaledVector(p.v, dt);
p.v.y -= CONFIG.gravity * dt;
p.m.rotation.x += dt * 4;
p.m.scale.setScalar(p.life);
if (p.life <= 0 || p.m.position.y < 0) {
scene.remove(p.m);
return false;
}
return true;
});
if (audio.initialized && (keys.w || keys.s || keys.a || keys.d) && isGrounded && Math.random() < dt * 6) audio.step();
mp.sync(camera.position, camera.quaternion);
updateUI();
}
drawMinimap();
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
updateConnStatus('Single Player', false);
updateUI();
animate();
</script>
</body>
</html>