470 lines
20 KiB
JavaScript
470 lines
20 KiB
JavaScript
/*!
|
|
* Tectum
|
|
* Copyright © 2026 Machiel Heinen.
|
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
|
* Zie LICENTIE.md.
|
|
*/
|
|
// ============================================================
|
|
// core/shooter.js — easter egg: schieten in de loopmodus (Ctrl+Shift+Alt+L).
|
|
// klik in beeld muis vangen (Esc = loslaten)
|
|
// linkermuisknop vuren
|
|
// 1 / 2 / scrollwiel pistool / raketwerper
|
|
// Kogelgaten en roetplekken blijven op wanden/dak/vloer achter, raketten
|
|
// laten ook wat rondvliegend metselwerk achter. Bewust simpel en buggy.
|
|
// Plain script (geen ES-module); gebruikt THREE.
|
|
//
|
|
// const gun = createShooter({ scene, camera, domElement, pane, walk, targets: () => [..] });
|
|
// gun.start(); gun.update(dt); gun.stop(); gun.dispose();
|
|
// ============================================================
|
|
|
|
function createShooter(o) {
|
|
const { scene, camera, domElement, pane, walk } = o;
|
|
const group = new THREE.Group();
|
|
group.name = 'shooter';
|
|
let active = false, weapon = 0, cooldown = 0, recoil = 0;
|
|
const decals = [], rockets = [], blasts = [], debris = [];
|
|
const MAX_DECALS = 250;
|
|
|
|
// --- Texturen (canvas) ---
|
|
function canvasTex(size, draw) {
|
|
const c = document.createElement('canvas');
|
|
c.width = c.height = size;
|
|
draw(c.getContext('2d'), size);
|
|
const t = new THREE.CanvasTexture(c);
|
|
return t;
|
|
}
|
|
const holeTex = canvasTex(64, (g, s) => {
|
|
const r = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
|
|
r.addColorStop(0, 'rgba(0,0,0,1)'); r.addColorStop(0.18, 'rgba(10,8,6,1)');
|
|
r.addColorStop(0.3, 'rgba(60,50,40,0.8)'); r.addColorStop(1, 'rgba(0,0,0,0)');
|
|
g.fillStyle = r; g.fillRect(0, 0, s, s);
|
|
g.strokeStyle = 'rgba(20,15,10,0.7)'; g.lineWidth = 1.5;
|
|
for (let i = 0; i < 6; i++) {
|
|
const a = Math.random() * Math.PI * 2, l = s * (0.25 + Math.random() * 0.2);
|
|
g.beginPath(); g.moveTo(s / 2, s / 2); g.lineTo(s / 2 + Math.cos(a) * l, s / 2 + Math.sin(a) * l); g.stroke();
|
|
}
|
|
});
|
|
const scorchTex = canvasTex(128, (g, s) => {
|
|
for (let i = 0; i < 40; i++) {
|
|
const x = s / 2 + (Math.random() - 0.5) * s * 0.5, y = s / 2 + (Math.random() - 0.5) * s * 0.5;
|
|
const rr = s * (0.1 + Math.random() * 0.25);
|
|
const r = g.createRadialGradient(x, y, 0, x, y, rr);
|
|
r.addColorStop(0, 'rgba(15,10,5,0.5)'); r.addColorStop(1, 'rgba(15,10,5,0)');
|
|
g.fillStyle = r; g.fillRect(0, 0, s, s);
|
|
}
|
|
g.fillStyle = 'rgba(255,120,20,0.6)';
|
|
for (let i = 0; i < 25; i++) g.fillRect(s / 2 + (Math.random() - 0.5) * s * 0.4, s / 2 + (Math.random() - 0.5) * s * 0.4, 2, 2);
|
|
});
|
|
const fireTex = canvasTex(64, (g, s) => {
|
|
const r = g.createRadialGradient(s / 2, s / 2, 0, s / 2, s / 2, s / 2);
|
|
r.addColorStop(0, 'rgba(255,255,220,1)'); r.addColorStop(0.3, 'rgba(255,180,40,0.9)');
|
|
r.addColorStop(0.7, 'rgba(200,50,0,0.4)'); r.addColorStop(1, 'rgba(0,0,0,0)');
|
|
g.fillStyle = r; g.fillRect(0, 0, s, s);
|
|
});
|
|
// Puin krijgt een willekeurige steentextuur uit de bibliotheek (als die laadt).
|
|
const brickTex = new THREE.TextureLoader().load(['assets/textures/buitenmuur/R200 - Witte voeg.jpg', 'assets/textures/buitenmuur/K739 - Witte voeg.jpg', 'assets/textures/buitenmuur/R480 - Witte voeg.jpg'][Math.floor(Math.random() * 3)]);
|
|
const decalMat = (map) => new THREE.MeshBasicMaterial({ map, transparent: true, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -4 });
|
|
const holeMat = decalMat(holeTex), scorchMat = decalMat(scorchTex);
|
|
const debrisMat = new THREE.MeshStandardMaterial({ map: brickTex, roughness: 0.9 });
|
|
const fireMat = new THREE.SpriteMaterial({ map: fireTex, blending: THREE.AdditiveBlending, depthWrite: false });
|
|
|
|
// --- Wapens (vastgeplakt aan de camera) ---
|
|
const metal = new THREE.MeshStandardMaterial({ color: 0x2a2c30, metalness: 0.7, roughness: 0.35 });
|
|
const olive = new THREE.MeshStandardMaterial({ color: 0x4b5a32, roughness: 0.6 });
|
|
function box(w, h, d, m, x, y, z) { const b = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), m); b.position.set(x, y, z); return b; }
|
|
const pistol = new THREE.Group();
|
|
pistol.add(box(0.035, 0.045, 0.2, metal, 0, 0, -0.1), box(0.03, 0.09, 0.04, metal, 0, -0.055, -0.02));
|
|
const launcher = new THREE.Group();
|
|
const tube = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 0.7, 12, 1, true), olive);
|
|
tube.material.side = THREE.DoubleSide;
|
|
tube.rotation.x = Math.PI / 2; tube.position.z = -0.25;
|
|
launcher.add(tube, box(0.03, 0.09, 0.04, metal, 0, -0.08, -0.05), box(0.02, 0.05, 0.03, metal, 0.05, 0.05, -0.2));
|
|
const flash = new THREE.Sprite(fireMat.clone());
|
|
flash.scale.set(0.15, 0.15, 1);
|
|
flash.visible = false;
|
|
const viewModel = new THREE.Group();
|
|
viewModel.add(pistol, launcher, flash);
|
|
const gunLight = new THREE.PointLight(0xffaa44, 0, 6);
|
|
group.add(viewModel, gunLight);
|
|
|
|
// --- HUD ---
|
|
const hud = document.createElement('div');
|
|
hud.className = 'shooter-hud';
|
|
hud.innerHTML = '<div class="shooter-cross"></div><div class="shooter-weapon"></div><div class="shooter-credit"></div><div class="shooter-kills"></div><div class="shooter-shout"></div><div class="shooter-ouch"></div><div class="shooter-announce"></div><div class="shooter-help">Klik = muis vangen · linkerknop = vuren · 1/2 of scroll = wapen · Esc = muis los · Ctrl+Shift+Alt+L = stoppen</div>';
|
|
pane.appendChild(hud);
|
|
hud.querySelector('.shooter-credit').textContent = GrannyKit.MUSIC_CREDIT;
|
|
const weaponLabel = hud.querySelector('.shooter-weapon');
|
|
const killsLabel = hud.querySelector('.shooter-kills');
|
|
const shoutLabel = hud.querySelector('.shooter-shout');
|
|
const ouch = hud.querySelector('.shooter-ouch');
|
|
let shoutTimer = 0;
|
|
function shout(text) { shoutLabel.textContent = text; shoutLabel.classList.add('show'); shoutTimer = 2.5; }
|
|
function setKills(n) { killsLabel.textContent = "Oma's: " + n; }
|
|
const announcer = GrannyKit.createAnnouncer(hud.querySelector('.shooter-announce'));
|
|
|
|
function selectWeapon(i) {
|
|
weapon = (i + 2) % 2;
|
|
pistol.visible = weapon === 0;
|
|
launcher.visible = weapon === 1;
|
|
weaponLabel.textContent = weapon === 0 ? '[1] Pistool ∞' : '[2] Raketwerper ∞';
|
|
flash.position.set(0, 0, weapon === 0 ? -0.22 : -0.62);
|
|
}
|
|
selectWeapon(0);
|
|
|
|
// --- Vijand: oud vrouwtje met rollator (8-bit sprite, Doom-stijl) ---
|
|
// Eén tegelijk: spawnt op 12-18 m, schuifelt op je af, na de kill komt de volgende.
|
|
function spriteTex(rows, tint) {
|
|
const t = new THREE.CanvasTexture(GrannyKit.pixelCanvas(rows, GrannyKit.PAL, { size: 32, tint, bottom: true }));
|
|
t.magFilter = t.minFilter = THREE.NearestFilter;
|
|
t.generateMipmaps = false;
|
|
return t;
|
|
}
|
|
const ladyTex = [spriteTex(GrannyKit.LADY), spriteTex(GrannyKit.LADY_B)];
|
|
const ladyHurtTex = spriteTex(GrannyKit.LADY, '#ff3030');
|
|
const LADY_H = 1.6 * 32 / 25; // sprite-hoogte (m) zodat de figuur ~1,6 m is
|
|
let mob = null, spawnTimer = 1.5, attackTimer = 0, animT = 0;
|
|
function spawnMob() {
|
|
const me = camera.position;
|
|
let x = me.x, z = me.z;
|
|
for (let i = 0; i < 15; i++) {
|
|
const a = Math.random() * Math.PI * 2, d = 12 + Math.random() * 6;
|
|
x = me.x + Math.sin(a) * d; z = me.z + Math.cos(a) * d;
|
|
if (!o.blocked || !o.blocked(x, z)) break;
|
|
}
|
|
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: ladyTex[0], transparent: true, alphaTest: 0.5 }));
|
|
sp.scale.set(LADY_H, LADY_H, 1);
|
|
group.add(sp);
|
|
mob = { sp, x, z, hp: 3, alive: true, hurt: 0, dead: 0 };
|
|
placeMob();
|
|
shout(GrannyKit.pick(GrannyKit.SHOUTS));
|
|
}
|
|
function mobFloor(x, z) { return o.floorAt ? o.floorAt(x, z) : 0; }
|
|
function placeMob() { mob.sp.position.set(mob.x, mobFloor(mob.x, mob.z) + LADY_H / 2 - (mob.dead ? LADY_H * 0.3 : 0), mob.z); }
|
|
function mobCenter() { return new THREE.Vector3(mob.x, mobFloor(mob.x, mob.z) + 0.8, mob.z); }
|
|
// Straal tegen een staande cilinder (r 0,4 m, 1,6 m hoog) om de oma.
|
|
function rayHitsMob(origin, dir, maxDist) {
|
|
if (!mob || !mob.alive) return false;
|
|
const c = mobCenter();
|
|
const t = c.clone().sub(origin).dot(dir);
|
|
if (t < 0 || t > maxDist) return false;
|
|
const p = origin.clone().addScaledVector(dir, t);
|
|
return Math.hypot(p.x - c.x, p.z - c.z) < 0.4 && Math.abs(p.y - c.y) < 0.8;
|
|
}
|
|
const gibTex = GrannyKit.GIBS.map(rows => {
|
|
const t = new THREE.CanvasTexture(GrannyKit.pixelCanvas(rows, GrannyKit.GIB_PAL, { size: 8 }));
|
|
t.magFilter = t.minFilter = THREE.NearestFilter;
|
|
t.generateMipmaps = false;
|
|
return t;
|
|
});
|
|
const bloodTex = canvasTex(32, (g) => {
|
|
g.fillStyle = '#7a0a0a';
|
|
for (let i = 0; i < 14; i++) {
|
|
const a = Math.random() * 6.28, d = Math.random() * 10, r = 2 + Math.random() * 5;
|
|
g.beginPath(); g.arc(16 + Math.cos(a) * d, 16 + Math.sin(a) * d, r, 0, 6.28); g.fill();
|
|
}
|
|
});
|
|
bloodTex.magFilter = THREE.NearestFilter;
|
|
const bloodMat = decalMat(bloodTex);
|
|
function spawnGibs(center, count) {
|
|
for (let i = 0; i < count; i++) {
|
|
// Vooral vlees, plus een paar herkenbare stukken.
|
|
const k = i < count * 0.5 ? Math.floor(Math.random() * 2) : Math.floor(Math.random() * GrannyKit.GIBS.length);
|
|
const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: gibTex[k], alphaTest: 0.5 }));
|
|
const sz = 0.18 + Math.random() * 0.14;
|
|
sp.scale.set(sz, sz, 1);
|
|
sp.position.copy(center).add(new THREE.Vector3((Math.random() - 0.5) * 0.4, (Math.random() - 0.5) * 0.8, (Math.random() - 0.5) * 0.4));
|
|
const v = new THREE.Vector3((Math.random() - 0.5) * 9, 2 + Math.random() * 6, (Math.random() - 0.5) * 9);
|
|
group.add(sp);
|
|
debris.push({ mesh: sp, v, spin: new THREE.Vector3((Math.random() - 0.5) * 20, 0, 0), rest: false, gib: true });
|
|
}
|
|
// Plas onder de plek des onheils.
|
|
bloodSplat(center.x, center.z, 1.4);
|
|
trimDebris();
|
|
}
|
|
function bloodSplat(x, z, size) {
|
|
const m = new THREE.Mesh(new THREE.PlaneGeometry(size, size), bloodMat);
|
|
m.rotation.set(-Math.PI / 2, 0, Math.random() * 6.28);
|
|
m.position.set(x, mobFloor(x, z) + 0.006, z);
|
|
group.add(m);
|
|
decals.push(m);
|
|
}
|
|
function trimDebris() {
|
|
while (debris.length > 200) {
|
|
const old = debris.shift();
|
|
group.remove(old.mesh);
|
|
if (old.mesh.isSprite) old.mesh.material.dispose(); else old.mesh.geometry.dispose();
|
|
}
|
|
}
|
|
|
|
function damageMob(n, gib) {
|
|
if (!mob || !mob.alive) return;
|
|
mob.hp -= n;
|
|
mob.hurt = 0.15;
|
|
spawnBlast(mobCenter(), 0.4, 0.2);
|
|
if (mob.hp > 0) { bang(0.2, 900, 0.3); return; }
|
|
mob.alive = false;
|
|
mob.dead = 0.001;
|
|
mob.sp.material.map = ladyHurtTex;
|
|
mob.sp.material.rotation = Math.PI / 2; // omgevallen
|
|
if (gib) { mob.sp.visible = false; mob.dead = 2.4; spawnGibs(mobCenter(), 22); }
|
|
else spawnGibs(mobCenter(), 5);
|
|
setKills(announcer.kill());
|
|
shout(GrannyKit.pick(GrannyKit.DEATH_SHOUTS));
|
|
spawnTimer = 1;
|
|
}
|
|
function updateMob(t) {
|
|
if (!mob) { spawnTimer -= t; if (spawnTimer <= 0) spawnMob(); return; }
|
|
if (!mob.alive) {
|
|
mob.dead += t;
|
|
mob.sp.material.opacity = Math.max(0, 1 - (mob.dead - 1.5));
|
|
mob.sp.material.transparent = true;
|
|
placeMob();
|
|
if (mob.dead > 2.5) { group.remove(mob.sp); mob.sp.material.dispose(); mob = null; }
|
|
return;
|
|
}
|
|
const me = camera.position;
|
|
const dx = me.x - mob.x, dz = me.z - mob.z, dist = Math.hypot(dx, dz);
|
|
if (dist > 0.9) {
|
|
const v = 1.3 * t / dist;
|
|
const nx = mob.x + dx * v, nz = mob.z + dz * v;
|
|
if (!o.blocked || !o.blocked(nx, nz)) { mob.x = nx; mob.z = nz; }
|
|
else if (!o.blocked(nx, mob.z)) mob.x = nx;
|
|
else if (!o.blocked(mob.x, nz)) mob.z = nz;
|
|
animT += t;
|
|
} else {
|
|
attackTimer -= t;
|
|
if (attackTimer <= 0) {
|
|
attackTimer = 1.2;
|
|
bang(0.1, 400, 0.6);
|
|
ouch.classList.remove('show'); void ouch.offsetWidth; ouch.classList.add('show');
|
|
announcer.hurt();
|
|
shout(GrannyKit.pick(GrannyKit.HIT_SHOUTS));
|
|
}
|
|
}
|
|
mob.hurt -= t;
|
|
mob.sp.material.map = mob.hurt > 0 ? ladyHurtTex : ladyTex[Math.floor(animT * 4) % 2];
|
|
placeMob();
|
|
}
|
|
|
|
const music = GrannyKit.createMusic();
|
|
const sfx = GrannyKit.createSfx();
|
|
const bang = sfx.bang;
|
|
|
|
// --- Raken ---
|
|
const ray = new THREE.Raycaster();
|
|
function hitScan(origin, dir, far) {
|
|
ray.set(origin, dir);
|
|
ray.far = far;
|
|
const hits = ray.intersectObjects(o.targets().filter(t => t && t.visible), true);
|
|
return hits.find(h => h.face && h.object.isMesh && isVisible(h.object)) || null;
|
|
}
|
|
function isVisible(obj) {
|
|
for (let p = obj; p; p = p.parent) if (!p.visible) return false;
|
|
return true;
|
|
}
|
|
function addDecal(hit, mat, size) {
|
|
const n = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
|
|
if (n.dot(ray.ray.direction) > 0) n.negate(); // altijd de kant waar je vandaan schiet
|
|
const m = new THREE.Mesh(new THREE.PlaneGeometry(size, size), mat);
|
|
m.position.copy(hit.point).addScaledVector(n, 0.004);
|
|
m.lookAt(hit.point.clone().add(n));
|
|
m.rotateZ(Math.random() * Math.PI * 2);
|
|
group.add(m);
|
|
decals.push(m);
|
|
if (decals.length > MAX_DECALS) { const old = decals.shift(); group.remove(old); old.geometry.dispose(); }
|
|
}
|
|
|
|
const tmpDir = new THREE.Vector3();
|
|
function fire() {
|
|
if (cooldown > 0) return;
|
|
camera.getWorldDirection(tmpDir);
|
|
// Lichte spreiding, het blijft een configurator.
|
|
tmpDir.x += (Math.random() - 0.5) * 0.01; tmpDir.y += (Math.random() - 0.5) * 0.01;
|
|
tmpDir.normalize();
|
|
flash.visible = true;
|
|
flash.material.rotation = Math.random() * 6;
|
|
gunLight.intensity = 2;
|
|
if (weapon === 0) {
|
|
cooldown = 0.18; recoil = 1;
|
|
bang(0.15, 3000, 0.5);
|
|
const hit = hitScan(camera.position, tmpDir, 200);
|
|
if (rayHitsMob(camera.position, tmpDir, hit ? hit.distance : 200)) damageMob(1);
|
|
else if (hit) {
|
|
addDecal(hit, holeMat, 0.05 + Math.random() * 0.03);
|
|
spawnBlast(hit.point, 0.15, 0.12);
|
|
}
|
|
} else {
|
|
cooldown = 0.8; recoil = 2.5;
|
|
bang(0.5, 600, 0.6);
|
|
const r = new THREE.Group();
|
|
r.add(box(0.06, 0.06, 0.3, olive, 0, 0, 0));
|
|
const tail = new THREE.Sprite(fireMat);
|
|
tail.scale.set(0.3, 0.3, 1); tail.position.z = 0.2;
|
|
r.add(tail);
|
|
r.position.copy(camera.position).addScaledVector(tmpDir, 0.6);
|
|
r.lookAt(r.position.clone().add(tmpDir));
|
|
group.add(r);
|
|
rockets.push({ mesh: r, dir: tmpDir.clone(), speed: 14, life: 6 });
|
|
}
|
|
}
|
|
|
|
function spawnBlast(pos, size, life) {
|
|
const s = new THREE.Sprite(fireMat.clone());
|
|
s.position.copy(pos);
|
|
s.scale.set(size, size, 1);
|
|
group.add(s);
|
|
blasts.push({ s, size, life, t: 0 });
|
|
}
|
|
|
|
function explode(hit, point, normal) {
|
|
bang(1.2, 300, 1);
|
|
if (hit) {
|
|
addDecal(hit, scorchMat, 1.2 + Math.random() * 0.6);
|
|
point = hit.point;
|
|
normal = hit.face.normal.clone().transformDirection(hit.object.matrixWorld);
|
|
if (normal.dot(ray.ray.direction) > 0) normal.negate();
|
|
}
|
|
spawnBlast(point, 2.5, 0.5);
|
|
const light = new THREE.PointLight(0xff8833, 6, 12);
|
|
light.position.copy(point);
|
|
group.add(light);
|
|
blasts.push({ light, life: 0.5, t: 0 });
|
|
if (mob && mob.alive && mobCenter().distanceTo(point) < 3) damageMob(99, true);
|
|
if (!hit) return; // direct op de oma: alleen gibs, geen stenen
|
|
const n = normal;
|
|
for (let i = 0; i < 12; i++) {
|
|
const sz = 0.05 + Math.random() * 0.12;
|
|
const d = new THREE.Mesh(new THREE.BoxGeometry(sz * 2, sz, sz), debrisMat);
|
|
d.position.copy(hit.point).addScaledVector(n, 0.1);
|
|
d.rotation.set(Math.random() * 6, Math.random() * 6, 0);
|
|
const v = n.clone().multiplyScalar(2 + Math.random() * 4).add(new THREE.Vector3((Math.random() - 0.5) * 6, Math.random() * 5, (Math.random() - 0.5) * 6));
|
|
group.add(d);
|
|
debris.push({ mesh: d, v, spin: new THREE.Vector3(Math.random() * 10, Math.random() * 10, 0), rest: false });
|
|
}
|
|
trimDebris();
|
|
}
|
|
|
|
// --- Invoer ---
|
|
function onClick(e) {
|
|
if (!active) return;
|
|
if (document.pointerLockElement !== domElement) { domElement.requestPointerLock && domElement.requestPointerLock(); return; }
|
|
if (e.button === 0) fire();
|
|
}
|
|
function onMouseMove(e) {
|
|
if (!active || document.pointerLockElement !== domElement) return;
|
|
walk.look(-e.movementX * 0.0025, -e.movementY * 0.0025);
|
|
}
|
|
function onWheel(e) {
|
|
if (!active) return;
|
|
e.preventDefault();
|
|
selectWeapon(weapon + (e.deltaY > 0 ? 1 : -1));
|
|
}
|
|
function onKey(e) {
|
|
if (!active || e.ctrlKey) return;
|
|
if (e.key === '1') selectWeapon(0);
|
|
if (e.key === '2') selectWeapon(1);
|
|
}
|
|
domElement.addEventListener('mousedown', onClick);
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
domElement.addEventListener('wheel', onWheel, { passive: false });
|
|
window.addEventListener('keydown', onKey);
|
|
|
|
function clear() {
|
|
if (mob) { group.remove(mob.sp); mob.sp.material.dispose(); mob = null; }
|
|
shoutLabel.classList.remove('show');
|
|
[...decals, ...debris.map(d => d.mesh), ...rockets.map(r => r.mesh), ...blasts.map(b => b.s || b.light)].forEach(m => group.remove(m));
|
|
decals.length = debris.length = rockets.length = blasts.length = 0;
|
|
}
|
|
|
|
return {
|
|
get active() { return active; },
|
|
start() {
|
|
active = true;
|
|
scene.add(group);
|
|
hud.classList.add('show');
|
|
announcer.reset();
|
|
setKills(0);
|
|
spawnTimer = 1.5;
|
|
music.play();
|
|
},
|
|
stop() {
|
|
active = false;
|
|
if (document.pointerLockElement === domElement) document.exitPointerLock();
|
|
clear();
|
|
scene.remove(group);
|
|
hud.classList.remove('show');
|
|
music.stop();
|
|
announcer.stop();
|
|
},
|
|
update(dt) {
|
|
if (!active) return;
|
|
const t = Math.min(dt, 0.05);
|
|
cooldown -= t;
|
|
// Wapen volgt de camera, met wat terugslag.
|
|
recoil = Math.max(0, recoil - t * 8);
|
|
viewModel.position.copy(camera.position);
|
|
viewModel.quaternion.copy(camera.quaternion);
|
|
viewModel.translateX(0.18); viewModel.translateY(-0.16 + recoil * 0.01); viewModel.translateZ(-0.25 + recoil * 0.03);
|
|
viewModel.rotateX(recoil * 0.08);
|
|
gunLight.position.copy(camera.position);
|
|
if (flash.visible && cooldown < (weapon === 0 ? 0.13 : 0.72)) flash.visible = false;
|
|
gunLight.intensity = Math.max(0, gunLight.intensity - t * 30);
|
|
announcer.update(t);
|
|
updateMob(t);
|
|
if (shoutTimer > 0 && (shoutTimer -= t) <= 0) shoutLabel.classList.remove('show');
|
|
|
|
for (let i = rockets.length - 1; i >= 0; i--) {
|
|
const r = rockets[i];
|
|
r.life -= t;
|
|
const stepLen = r.speed * t;
|
|
r.speed += 10 * t;
|
|
const hit = hitScan(r.mesh.position, r.dir, stepLen);
|
|
if (rayHitsMob(r.mesh.position, r.dir, stepLen + 0.3)) {
|
|
explode(null, mobCenter(), r.dir.clone().negate());
|
|
group.remove(r.mesh);
|
|
rockets.splice(i, 1);
|
|
} else if (hit || r.life <= 0) {
|
|
if (hit) explode(hit);
|
|
group.remove(r.mesh);
|
|
rockets.splice(i, 1);
|
|
} else {
|
|
r.mesh.position.addScaledVector(r.dir, stepLen);
|
|
if (Math.random() < 0.5) spawnBlast(r.mesh.position.clone().addScaledVector(r.dir, -0.2), 0.2, 0.3);
|
|
}
|
|
}
|
|
for (let i = blasts.length - 1; i >= 0; i--) {
|
|
const b = blasts[i];
|
|
b.t += t;
|
|
const k = b.t / b.life;
|
|
if (b.s) { b.s.scale.setScalar(b.size * (0.6 + k)); b.s.material.opacity = 1 - k; }
|
|
if (b.light) b.light.intensity = 6 * (1 - k);
|
|
if (k >= 1) { group.remove(b.s || b.light); if (b.s) b.s.material.dispose(); blasts.splice(i, 1); }
|
|
}
|
|
for (const d of debris) {
|
|
if (d.rest) continue;
|
|
d.v.y -= 9.81 * t;
|
|
const next = d.mesh.position.clone().addScaledVector(d.v, t);
|
|
const floor = (o.floorAt ? o.floorAt(next.x, next.z) : 0) + (d.gib ? d.mesh.scale.y * 0.35 : 0.03);
|
|
if (next.y <= floor && d.v.y < 0) {
|
|
next.y = floor;
|
|
d.v.multiplyScalar(0.4); d.v.y = -d.v.y * 0.5; d.spin.multiplyScalar(0.5);
|
|
if (d.gib && !d.splat) { d.splat = true; bloodSplat(next.x, next.z, 0.3 + Math.random() * 0.3); }
|
|
if (d.v.length() < 0.3) d.rest = true;
|
|
}
|
|
d.mesh.position.copy(next);
|
|
if (d.mesh.isSprite) d.mesh.material.rotation += d.spin.x * t;
|
|
else { d.mesh.rotation.x += d.spin.x * t; d.mesh.rotation.y += d.spin.y * t; }
|
|
}
|
|
},
|
|
dispose() {
|
|
this.stop();
|
|
domElement.removeEventListener('mousedown', onClick);
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
domElement.removeEventListener('wheel', onWheel);
|
|
window.removeEventListener('keydown', onKey);
|
|
hud.remove();
|
|
sfx.close();
|
|
}
|
|
};
|
|
}
|