1343 lines
44 KiB
JavaScript
Executable File
1343 lines
44 KiB
JavaScript
Executable File
/**
|
|
* DOOM 3D - Arsenal & Weapon System (CS:GO / Classic High-Detail Right-Handed Viewmodels)
|
|
* 7 Iconic weapons with detailed right-handed perspective, gloved marine hands,
|
|
* slide blowbacks, pump actions, spinning gatling barrels, and dynamic muzzle flashes.
|
|
*/
|
|
|
|
class DoomWeapons {
|
|
constructor(engine, player, sound) {
|
|
this.engine = engine;
|
|
this.player = player;
|
|
this.sound = sound;
|
|
|
|
// Weapon Canvas Overlay for 1st Person Right-Handed Viewmodels
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.id = 'weapon-canvas';
|
|
this.ctx = this.canvas.getContext('2d');
|
|
document.getElementById('game-container').appendChild(this.canvas);
|
|
this.resizeCanvas();
|
|
|
|
window.addEventListener('resize', () => this.resizeCanvas());
|
|
|
|
// 3D Projectiles in World
|
|
this.projectiles = [];
|
|
this.ejectedCasings = [];
|
|
|
|
// Weapon Arsenal Definitions
|
|
this.weapons = [
|
|
{
|
|
id: 'fist',
|
|
name: 'COMBAT KNIFE / FISTS',
|
|
type: 'melee',
|
|
ammoType: null,
|
|
ammoCost: 0,
|
|
damage: 35,
|
|
fireRate: 0.35,
|
|
range: 3.0
|
|
},
|
|
{
|
|
id: 'pistol',
|
|
name: 'COMBAT PISTOL',
|
|
type: 'hitscan',
|
|
ammoType: 'bullets',
|
|
ammoCost: 1,
|
|
damage: 18,
|
|
fireRate: 0.26,
|
|
spread: 0.012,
|
|
pellets: 1
|
|
},
|
|
{
|
|
id: 'shotgun',
|
|
name: 'PUMP-ACTION SHOTGUN',
|
|
type: 'hitscan',
|
|
ammoType: 'shells',
|
|
ammoCost: 1,
|
|
damage: 16,
|
|
fireRate: 0.85,
|
|
spread: 0.055,
|
|
pellets: 9
|
|
},
|
|
{
|
|
id: 'chaingun',
|
|
name: 'ROTARY CHAINGUN',
|
|
type: 'hitscan',
|
|
ammoType: 'bullets',
|
|
ammoCost: 1,
|
|
damage: 16,
|
|
fireRate: 0.09,
|
|
spread: 0.038,
|
|
pellets: 1
|
|
},
|
|
{
|
|
id: 'rocket',
|
|
name: 'ROCKET LAUNCHER',
|
|
type: 'projectile',
|
|
ammoType: 'rockets',
|
|
ammoCost: 1,
|
|
damage: 160,
|
|
splashRadius: 7.0,
|
|
fireRate: 0.8,
|
|
speed: 30.0
|
|
},
|
|
{
|
|
id: 'plasma',
|
|
name: 'PLASMA RIFLE',
|
|
type: 'projectile',
|
|
ammoType: 'cells',
|
|
ammoCost: 1,
|
|
damage: 38,
|
|
splashRadius: 1.8,
|
|
fireRate: 0.11,
|
|
speed: 42.0
|
|
},
|
|
{
|
|
id: 'bfg',
|
|
name: 'BFG 9000',
|
|
type: 'bfg',
|
|
ammoType: 'cells',
|
|
ammoCost: 40,
|
|
damage: 650,
|
|
splashRadius: 15.0,
|
|
fireRate: 1.6,
|
|
speed: 24.0
|
|
}
|
|
];
|
|
|
|
// Unlocked Weapons (Fists, Pistol, Shotgun initially unlocked)
|
|
this.unlocked = [true, true, true, false, false, false, false];
|
|
this.currentIndex = 1; // Default to Pistol
|
|
this.nextIndex = 1;
|
|
|
|
// Firing & Animation States
|
|
this.fireTimer = 0;
|
|
this.recoilOffset = 0;
|
|
this.pumpProgress = 0;
|
|
this.slideOffset = 0;
|
|
this.gatlingAngle = 0;
|
|
this.gatlingSpinSpeed = 0;
|
|
this.muzzleFlashTimer = 0;
|
|
this.switchProgress = 1.0;
|
|
this.isSwitching = false;
|
|
this.knifeSlashTimer = 0;
|
|
this.bfgCharging = false;
|
|
this.bfgChargeTimer = 0;
|
|
|
|
// Inertia Sway & Bobbing
|
|
this.swayX = 0;
|
|
this.swayY = 0;
|
|
this.lastMouseX = 0;
|
|
this.lastMouseY = 0;
|
|
|
|
// Dynamic 3D Muzzle Light
|
|
this.muzzleLight = new THREE.PointLight(0xffaa22, 0, 16, 2);
|
|
this.engine.scene.add(this.muzzleLight);
|
|
|
|
this.setupInput();
|
|
}
|
|
|
|
resizeCanvas() {
|
|
const w = window.innerWidth || 800;
|
|
const h = Math.max(300, (window.innerHeight || 600) - 85);
|
|
this.canvas.width = w;
|
|
this.canvas.height = h;
|
|
}
|
|
|
|
get current() {
|
|
return this.weapons[this.currentIndex];
|
|
}
|
|
|
|
unlockWeapon(index) {
|
|
if (index >= 0 && index < this.weapons.length) {
|
|
this.unlocked[index] = true;
|
|
this.selectWeapon(index);
|
|
}
|
|
}
|
|
|
|
selectWeapon(index) {
|
|
if (index < 0 || index >= this.weapons.length || !this.unlocked[index]) return;
|
|
if (index === this.currentIndex && !this.isSwitching) return;
|
|
|
|
const w = this.weapons[index];
|
|
if (w.ammoType && this.player.ammo[w.ammoType] < w.ammoCost) {
|
|
return;
|
|
}
|
|
|
|
this.nextIndex = index;
|
|
this.isSwitching = true;
|
|
if (this.sound) this.sound.playItemPickup();
|
|
}
|
|
|
|
nextWeapon() {
|
|
let idx = (this.currentIndex + 1) % this.weapons.length;
|
|
while (idx !== this.currentIndex) {
|
|
if (this.unlocked[idx]) {
|
|
const w = this.weapons[idx];
|
|
if (!w.ammoType || this.player.ammo[w.ammoType] >= w.ammoCost) {
|
|
this.selectWeapon(idx);
|
|
return;
|
|
}
|
|
}
|
|
idx = (idx + 1) % this.weapons.length;
|
|
}
|
|
}
|
|
|
|
prevWeapon() {
|
|
let idx = (this.currentIndex - 1 + this.weapons.length) % this.weapons.length;
|
|
while (idx !== this.currentIndex) {
|
|
if (this.unlocked[idx]) {
|
|
const w = this.weapons[idx];
|
|
if (!w.ammoType || this.player.ammo[w.ammoType] >= w.ammoCost) {
|
|
this.selectWeapon(idx);
|
|
return;
|
|
}
|
|
}
|
|
idx = (idx - 1 + this.weapons.length) % this.weapons.length;
|
|
}
|
|
}
|
|
|
|
setupInput() {
|
|
// Mouse Wheel weapon cycle
|
|
window.addEventListener('wheel', (e) => {
|
|
if (e.deltaY > 0) this.nextWeapon();
|
|
else if (e.deltaY < 0) this.prevWeapon();
|
|
});
|
|
|
|
// Numeric Keys (1-7)
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code >= 'Digit1' && e.code <= 'Digit7') {
|
|
const num = parseInt(e.code.replace('Digit', '')) - 1;
|
|
this.selectWeapon(num);
|
|
}
|
|
});
|
|
|
|
// Mouse move inertia tracking
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (this.player && this.player.isPointerLocked) {
|
|
const mx = e.movementX || 0;
|
|
const my = e.movementY || 0;
|
|
this.swayX = THREE.MathUtils.clamp(this.swayX - mx * 0.15, -25, 25);
|
|
this.swayY = THREE.MathUtils.clamp(this.swayY - my * 0.15, -20, 20);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// FIRING EXECUTION
|
|
// ----------------------------------------------------
|
|
|
|
triggerAttack() {
|
|
if (this.player.isDead || this.isSwitching || this.fireTimer > 0) return;
|
|
|
|
const w = this.current;
|
|
|
|
// Check ammo
|
|
if (w.ammoType && this.player.ammo[w.ammoType] < w.ammoCost) {
|
|
if (this.sound) this.sound.playSlimeHurt();
|
|
this.prevWeapon();
|
|
return;
|
|
}
|
|
|
|
// Deduct ammo
|
|
if (w.ammoType) {
|
|
this.player.ammo[w.ammoType] -= w.ammoCost;
|
|
}
|
|
|
|
this.fireTimer = w.fireRate;
|
|
this.recoilOffset = 1.0;
|
|
this.muzzleFlashTimer = 0.08;
|
|
|
|
// HUD face attack grimace
|
|
if (window.game && window.game.hud) {
|
|
window.game.hud.triggerAttackState();
|
|
}
|
|
|
|
if (w.id === 'fist') {
|
|
this.knifeSlashTimer = 1.0;
|
|
if (this.sound) this.sound.playPistol();
|
|
this.performMeleeAttack(w);
|
|
} else if (w.id === 'pistol') {
|
|
this.slideOffset = 1.0;
|
|
if (this.sound) this.sound.playPistol();
|
|
this.triggerMuzzleLight(0xffcc00);
|
|
this.spawnEjectedCasing('bullet');
|
|
this.performHitscanAttack(w);
|
|
} else if (w.id === 'shotgun') {
|
|
this.pumpProgress = 1.0;
|
|
if (this.sound) this.sound.playShotgun();
|
|
this.triggerMuzzleLight(0xffaa00);
|
|
this.spawnEjectedCasing('shell');
|
|
this.performHitscanAttack(w);
|
|
} else if (w.id === 'chaingun') {
|
|
this.gatlingSpinSpeed = 25.0;
|
|
if (this.sound) this.sound.playChaingun();
|
|
this.triggerMuzzleLight(0xffea00);
|
|
this.spawnEjectedCasing('bullet');
|
|
this.performHitscanAttack(w);
|
|
} else if (w.id === 'rocket') {
|
|
if (this.sound) this.sound.playRocketFire();
|
|
this.triggerMuzzleLight(0xff5500);
|
|
this.launchRocketProjectile(w);
|
|
} else if (w.id === 'plasma') {
|
|
if (this.sound) this.sound.playPlasmaFire();
|
|
this.triggerMuzzleLight(0x00d4ff);
|
|
this.launchPlasmaProjectile(w);
|
|
} else if (w.id === 'bfg') {
|
|
this.bfgCharging = true;
|
|
this.bfgChargeTimer = 0.6;
|
|
if (this.sound) this.sound.playBFGCharge();
|
|
}
|
|
}
|
|
|
|
triggerMuzzleLight(color) {
|
|
this.muzzleLight.color.setHex(color);
|
|
this.muzzleLight.intensity = 4.5;
|
|
this.muzzleLight.position.copy(this.player.camera.position);
|
|
}
|
|
|
|
spawnEjectedCasing(type) {
|
|
this.ejectedCasings.push({
|
|
type,
|
|
x: 0,
|
|
y: 0,
|
|
vx: 3 + Math.random() * 4,
|
|
vy: -4 - Math.random() * 4,
|
|
rot: 0,
|
|
vRot: 10 + Math.random() * 20,
|
|
life: 0.6
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// MELEE, HITSCAN & PROJECTILE MECHANICS
|
|
// ----------------------------------------------------
|
|
|
|
performMeleeAttack(w) {
|
|
const cam = this.player.camera;
|
|
const origin = cam.position.clone();
|
|
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
|
|
const hit = this.raycastAttack(origin, dir, w.range, w.damage);
|
|
if (hit && this.sound) {
|
|
this.sound.playSlimeHurt();
|
|
}
|
|
}
|
|
|
|
performHitscanAttack(w) {
|
|
const cam = this.player.camera;
|
|
const origin = cam.position.clone();
|
|
const baseDir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
|
|
for (let i = 0; i < w.pellets; i++) {
|
|
const spreadX = (Math.random() - 0.5) * w.spread;
|
|
const spreadY = (Math.random() - 0.5) * w.spread;
|
|
const dir = baseDir.clone().add(new THREE.Vector3(spreadX, spreadY, 0).applyQuaternion(cam.quaternion)).normalize();
|
|
|
|
this.raycastAttack(origin, dir, 80, w.damage);
|
|
}
|
|
}
|
|
|
|
raycastAttack(origin, dir, maxDist, damage) {
|
|
let nearestHit = null;
|
|
let nearestDist = maxDist;
|
|
let hitType = null;
|
|
let hitTarget = null;
|
|
|
|
// Check enemies
|
|
if (window.game && window.game.enemies) {
|
|
const enemies = window.game.enemies.enemies;
|
|
for (let i = 0; i < enemies.length; i++) {
|
|
const e = enemies[i];
|
|
if (e.isDead) continue;
|
|
|
|
const toEnemy = e.mesh.position.clone().sub(origin);
|
|
const proj = toEnemy.dot(dir);
|
|
if (proj > 0 && proj < nearestDist) {
|
|
const perp = toEnemy.sub(dir.clone().multiplyScalar(proj));
|
|
if (perp.length() < e.radius) {
|
|
nearestDist = proj;
|
|
nearestHit = origin.clone().addScaledVector(dir, proj);
|
|
hitType = 'enemy';
|
|
hitTarget = e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check explosive barrels
|
|
if (window.game && window.game.items) {
|
|
const barrels = window.game.items.barrels;
|
|
for (let i = 0; i < barrels.length; i++) {
|
|
const b = barrels[i];
|
|
if (b.isExploded) continue;
|
|
|
|
const toBarrel = b.mesh.position.clone().sub(origin);
|
|
const proj = toBarrel.dot(dir);
|
|
if (proj > 0 && proj < nearestDist) {
|
|
const perp = toBarrel.sub(dir.clone().multiplyScalar(proj));
|
|
if (perp.length() < 0.6) {
|
|
nearestDist = proj;
|
|
nearestHit = origin.clone().addScaledVector(dir, proj);
|
|
hitType = 'barrel';
|
|
hitTarget = b;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check level walls
|
|
if (window.game && window.game.map) {
|
|
const colliders = window.game.map.getColliders();
|
|
const ray = new THREE.Ray(origin, dir);
|
|
const targetPos = new THREE.Vector3();
|
|
|
|
for (let i = 0; i < colliders.length; i++) {
|
|
const box = colliders[i];
|
|
if (ray.intersectBox(box, targetPos)) {
|
|
const dist = origin.distanceTo(targetPos);
|
|
if (dist < nearestDist) {
|
|
nearestDist = dist;
|
|
nearestHit = targetPos.clone();
|
|
hitType = 'wall';
|
|
hitTarget = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nearestHit) {
|
|
if (hitType === 'enemy') {
|
|
hitTarget.takeDamage(damage, nearestHit);
|
|
this.engine.createBloodBurst(nearestHit, dir.clone().negate());
|
|
} else if (hitType === 'barrel') {
|
|
hitTarget.takeDamage(damage);
|
|
this.engine.createSparks(nearestHit, new THREE.Vector3(0, 1, 0));
|
|
} else if (hitType === 'wall') {
|
|
this.engine.createSparks(nearestHit, dir.clone().negate());
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
launchRocketProjectile(w) {
|
|
const cam = this.player.camera;
|
|
const origin = cam.position.clone().addScaledVector(new THREE.Vector3(0.4, -0.3, -0.6).applyQuaternion(cam.quaternion), 1);
|
|
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
|
|
const geo = new THREE.CylinderGeometry(0.12, 0.12, 0.7, 8);
|
|
const mat = new THREE.MeshStandardMaterial({ color: 0x4a5d38, roughness: 0.4 });
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
mesh.position.copy(origin);
|
|
this.engine.scene.add(mesh);
|
|
|
|
const light = new THREE.PointLight(0xff5500, 3.5, 12, 2);
|
|
light.position.copy(origin);
|
|
this.engine.scene.add(light);
|
|
|
|
this.projectiles.push({
|
|
type: 'rocket',
|
|
mesh,
|
|
light,
|
|
position: origin,
|
|
velocity: dir.clone().multiplyScalar(w.speed),
|
|
damage: w.damage,
|
|
splashRadius: w.splashRadius,
|
|
life: 5.0
|
|
});
|
|
}
|
|
|
|
launchPlasmaProjectile(w) {
|
|
const cam = this.player.camera;
|
|
const origin = cam.position.clone().addScaledVector(new THREE.Vector3(0.35, -0.25, -0.5).applyQuaternion(cam.quaternion), 1);
|
|
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
|
|
const geo = new THREE.SphereGeometry(0.28, 8, 8);
|
|
const mat = new THREE.MeshBasicMaterial({ color: 0x00d4ff });
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.position.copy(origin);
|
|
this.engine.scene.add(mesh);
|
|
|
|
const light = new THREE.PointLight(0x00d4ff, 3.0, 10, 2);
|
|
light.position.copy(origin);
|
|
this.engine.scene.add(light);
|
|
|
|
this.projectiles.push({
|
|
type: 'plasma',
|
|
mesh,
|
|
light,
|
|
position: origin,
|
|
velocity: dir.clone().multiplyScalar(w.speed),
|
|
damage: w.damage,
|
|
splashRadius: w.splashRadius,
|
|
life: 4.0
|
|
});
|
|
}
|
|
|
|
executeBFGFire() {
|
|
const w = this.weapons[6];
|
|
if (this.sound) this.sound.playBFGFire();
|
|
this.triggerMuzzleLight(0x00ff44);
|
|
|
|
const cam = this.player.camera;
|
|
const origin = cam.position.clone().addScaledVector(new THREE.Vector3(0.3, -0.2, -0.8).applyQuaternion(cam.quaternion), 1);
|
|
const dir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion).normalize();
|
|
|
|
const geo = new THREE.SphereGeometry(0.75, 16, 16);
|
|
const mat = new THREE.MeshStandardMaterial({
|
|
color: 0x00ff44,
|
|
emissive: 0x00ff44,
|
|
emissiveIntensity: 1.0,
|
|
transparent: true,
|
|
opacity: 0.9
|
|
});
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.position.copy(origin);
|
|
this.engine.scene.add(mesh);
|
|
|
|
const light = new THREE.PointLight(0x00ff44, 6.0, 20, 2);
|
|
light.position.copy(origin);
|
|
this.engine.scene.add(light);
|
|
|
|
this.projectiles.push({
|
|
type: 'bfg',
|
|
mesh,
|
|
light,
|
|
position: origin,
|
|
velocity: dir.clone().multiplyScalar(w.speed),
|
|
damage: w.damage,
|
|
splashRadius: w.splashRadius,
|
|
life: 6.0
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// PROJECTILES & CASINGS UPDATE LOOP
|
|
// ----------------------------------------------------
|
|
|
|
updateProjectiles(delta) {
|
|
// Projectiles
|
|
for (let i = this.projectiles.length - 1; i >= 0; i--) {
|
|
const p = this.projectiles[i];
|
|
p.mesh.position.addScaledVector(p.velocity, delta);
|
|
p.light.position.copy(p.mesh.position);
|
|
p.life -= delta;
|
|
|
|
if (p.type === 'rocket') {
|
|
this.engine.createSmokeParticle(p.mesh.position);
|
|
}
|
|
|
|
let collided = false;
|
|
const hitPos = p.mesh.position.clone();
|
|
|
|
// Collision with enemies
|
|
if (window.game && window.game.enemies) {
|
|
const enemies = window.game.enemies.enemies;
|
|
for (let j = 0; j < enemies.length; j++) {
|
|
const e = enemies[j];
|
|
if (e.isDead) continue;
|
|
if (e.mesh.position.distanceTo(hitPos) < e.radius + 0.5) {
|
|
collided = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collision with walls
|
|
if (!collided && window.game && window.game.map) {
|
|
const colliders = window.game.map.getColliders();
|
|
for (let j = 0; j < colliders.length; j++) {
|
|
if (colliders[j].containsPoint(hitPos)) {
|
|
collided = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (collided || p.life <= 0) {
|
|
this.detonateProjectile(p, hitPos);
|
|
this.destroyProjectile(i);
|
|
}
|
|
}
|
|
|
|
// Casings
|
|
for (let i = this.ejectedCasings.length - 1; i >= 0; i--) {
|
|
const c = this.ejectedCasings[i];
|
|
c.x += c.vx * delta * 60;
|
|
c.y += c.vy * delta * 60;
|
|
c.vy += delta * 18; // gravity
|
|
c.rot += c.vRot * delta;
|
|
c.life -= delta;
|
|
if (c.life <= 0) {
|
|
this.ejectedCasings.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
detonateProjectile(p, pos) {
|
|
if (p.type === 'rocket') {
|
|
if (this.sound) this.sound.playRocketExplode();
|
|
this.engine.createExplosionFX(pos, 0xff5500);
|
|
this.applySplashDamage(pos, p.splashRadius, p.damage);
|
|
} else if (p.type === 'plasma') {
|
|
this.engine.createExplosionFX(pos, 0x00d4ff);
|
|
this.applySplashDamage(pos, p.splashRadius, p.damage);
|
|
} else if (p.type === 'bfg') {
|
|
if (this.sound) this.sound.playRocketExplode();
|
|
this.engine.createExplosionFX(pos, 0x00ff44);
|
|
this.applySplashDamage(pos, p.splashRadius, p.damage);
|
|
|
|
// BFG secondary cone trace
|
|
if (window.game && window.game.enemies) {
|
|
const enemies = window.game.enemies.enemies;
|
|
const cam = this.player.camera;
|
|
const pDir = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.quaternion);
|
|
|
|
for (let i = 0; i < enemies.length; i++) {
|
|
const e = enemies[i];
|
|
if (e.isDead) continue;
|
|
const toE = e.mesh.position.clone().sub(cam.position);
|
|
const dist = toE.length();
|
|
if (dist < 40) {
|
|
const angle = pDir.angleTo(toE);
|
|
if (angle < Math.PI / 3) {
|
|
e.takeDamage(250 + Math.floor(Math.random() * 200), e.mesh.position);
|
|
this.engine.createExplosionFX(e.mesh.position, 0x00ff44);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
applySplashDamage(pos, radius, maxDamage) {
|
|
if (window.game && window.game.enemies) {
|
|
const enemies = window.game.enemies.enemies;
|
|
for (let i = 0; i < enemies.length; i++) {
|
|
const e = enemies[i];
|
|
if (e.isDead) continue;
|
|
const dist = pos.distanceTo(e.mesh.position);
|
|
if (dist < radius) {
|
|
const factor = 1.0 - (dist / radius);
|
|
e.takeDamage(Math.floor(maxDamage * factor), pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (window.game && window.game.items) {
|
|
const barrels = window.game.items.barrels;
|
|
for (let i = 0; i < barrels.length; i++) {
|
|
const b = barrels[i];
|
|
if (b.isExploded) continue;
|
|
const dist = pos.distanceTo(b.mesh.position);
|
|
if (dist < radius) {
|
|
b.takeDamage(maxDamage);
|
|
}
|
|
}
|
|
}
|
|
|
|
const pDist = pos.distanceTo(this.player.position);
|
|
if (pDist < radius) {
|
|
const factor = 1.0 - (pDist / radius);
|
|
this.player.takeDamage(Math.floor(maxDamage * 0.5 * factor), pos);
|
|
}
|
|
}
|
|
|
|
destroyProjectile(index) {
|
|
const p = this.projectiles[index];
|
|
this.engine.scene.remove(p.mesh);
|
|
this.engine.scene.remove(p.light);
|
|
this.projectiles.splice(index, 1);
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// RIGHT-HANDED HIGH-DETAIL VIEWMODEL RENDERING (CS:GO / DOOM)
|
|
// ----------------------------------------------------
|
|
|
|
update(delta) {
|
|
// Fire Timer
|
|
if (this.fireTimer > 0) {
|
|
this.fireTimer -= delta;
|
|
}
|
|
|
|
// BFG Charge
|
|
if (this.bfgCharging) {
|
|
this.bfgChargeTimer -= delta;
|
|
if (this.bfgChargeTimer <= 0) {
|
|
this.bfgCharging = false;
|
|
this.executeBFGFire();
|
|
}
|
|
}
|
|
|
|
// Full Auto Firing
|
|
if (this.player.keys.shoot && this.fireTimer <= 0 && !this.isSwitching) {
|
|
if (this.current.id === 'chaingun' || this.current.id === 'plasma') {
|
|
this.triggerAttack();
|
|
}
|
|
}
|
|
|
|
// Weapon Switch Animation
|
|
if (this.isSwitching) {
|
|
this.switchProgress -= delta * 5.5;
|
|
if (this.switchProgress <= 0) {
|
|
this.currentIndex = this.nextIndex;
|
|
this.isSwitching = false;
|
|
this.switchProgress = 0;
|
|
}
|
|
} else if (this.switchProgress < 1.0) {
|
|
this.switchProgress = Math.min(1.0, this.switchProgress + delta * 5.5);
|
|
}
|
|
|
|
// Recoil Recovery
|
|
if (this.recoilOffset > 0) {
|
|
this.recoilOffset = Math.max(0, this.recoilOffset - delta * 5.0);
|
|
}
|
|
if (this.slideOffset > 0) {
|
|
this.slideOffset = Math.max(0, this.slideOffset - delta * 8.0);
|
|
}
|
|
if (this.pumpProgress > 0) {
|
|
this.pumpProgress = Math.max(0, this.pumpProgress - delta * 2.2);
|
|
}
|
|
if (this.knifeSlashTimer > 0) {
|
|
this.knifeSlashTimer = Math.max(0, this.knifeSlashTimer - delta * 3.5);
|
|
}
|
|
|
|
// Gatling Spin
|
|
if (this.gatlingSpinSpeed > 0) {
|
|
this.gatlingAngle += this.gatlingSpinSpeed * delta;
|
|
this.gatlingSpinSpeed = Math.max(0, this.gatlingSpinSpeed - delta * 15.0);
|
|
}
|
|
|
|
// Muzzle Flash Timer
|
|
if (this.muzzleFlashTimer > 0) {
|
|
this.muzzleFlashTimer -= delta;
|
|
if (this.muzzleFlashTimer <= 0) {
|
|
this.muzzleLight.intensity = 0;
|
|
}
|
|
}
|
|
|
|
// Inertia Sway damping
|
|
this.swayX *= 0.88;
|
|
this.swayY *= 0.88;
|
|
|
|
this.updateProjectiles(delta);
|
|
this.renderViewmodel();
|
|
}
|
|
|
|
renderViewmodel() {
|
|
const ctx = this.ctx;
|
|
const w = this.canvas.width;
|
|
const h = this.canvas.height;
|
|
ctx.clearRect(0, 0, w, h);
|
|
|
|
if (this.player.isDead) return;
|
|
|
|
// Base Viewmodel Anchor (Right-Handed CS:GO style)
|
|
const baseAnchorX = w * 0.68;
|
|
const baseAnchorY = h;
|
|
|
|
// Bobbing sway from movement
|
|
const bobX = this.player.bobOffset.x * 120 + this.swayX;
|
|
const bobY = Math.abs(this.player.bobOffset.y) * 90 + this.swayY;
|
|
|
|
// Recoil kickback
|
|
const recoilY = -this.recoilOffset * 28;
|
|
const recoilX = -this.recoilOffset * 10;
|
|
const recoilRot = -this.recoilOffset * 0.12;
|
|
|
|
// Switch lowering offset
|
|
const switchY = (1.0 - this.switchProgress) * (h * 0.6);
|
|
|
|
ctx.save();
|
|
ctx.translate(baseAnchorX + bobX + recoilX, baseAnchorY + bobY + recoilY + switchY);
|
|
ctx.rotate(recoilRot);
|
|
|
|
const currentWpn = this.current.id;
|
|
|
|
if (currentWpn === 'fist') {
|
|
this.drawCombatKnife(ctx);
|
|
} else if (currentWpn === 'pistol') {
|
|
this.drawPistolViewmodel(ctx);
|
|
} else if (currentWpn === 'shotgun') {
|
|
this.drawShotgunViewmodel(ctx);
|
|
} else if (currentWpn === 'chaingun') {
|
|
this.drawChaingunViewmodel(ctx);
|
|
} else if (currentWpn === 'rocket') {
|
|
this.drawRocketViewmodel(ctx);
|
|
} else if (currentWpn === 'plasma') {
|
|
this.drawPlasmaViewmodel(ctx);
|
|
} else if (currentWpn === 'bfg') {
|
|
this.drawBFGViewmodel(ctx);
|
|
}
|
|
|
|
// Draw Muzzle Flash Burst
|
|
if (this.muzzleFlashTimer > 0 && currentWpn !== 'fist') {
|
|
this.drawMuzzleFlashBurst(ctx, currentWpn);
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
// Render Flying Shell Casings
|
|
this.renderCasings(ctx, baseAnchorX + bobX, baseAnchorY - 140);
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// DETAILED GLOVES & SLEEVES (CS:GO CT/Doom Marine Style)
|
|
// ----------------------------------------------------
|
|
|
|
drawMarineHand(ctx, x, y, angle = 0, gripFingers = 4) {
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(angle);
|
|
|
|
// Tactical Camo / Olive Military Sleeve
|
|
const sleeveGrad = ctx.createLinearGradient(-40, 60, 80, 200);
|
|
sleeveGrad.addColorStop(0, '#2b3624');
|
|
sleeveGrad.addColorStop(0.5, '#1e261a');
|
|
sleeveGrad.addColorStop(1, '#11160e');
|
|
|
|
ctx.fillStyle = sleeveGrad;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-50, 240);
|
|
ctx.lineTo(-25, 60);
|
|
ctx.lineTo(65, 75);
|
|
ctx.lineTo(85, 240);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Sleeve Fabric Folds & Stitching
|
|
ctx.strokeStyle = '#141811';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(-35, 120);
|
|
ctx.lineTo(40, 135);
|
|
ctx.moveTo(-20, 180);
|
|
ctx.lineTo(55, 195);
|
|
ctx.stroke();
|
|
|
|
// Glove Cuff
|
|
ctx.fillStyle = '#1b1d22';
|
|
ctx.fillRect(-28, 52, 95, 18);
|
|
|
|
// Combat Glove Palm & Knuckles
|
|
const gloveGrad = ctx.createLinearGradient(-20, 0, 60, 60);
|
|
gloveGrad.addColorStop(0, '#2e323b');
|
|
gloveGrad.addColorStop(1, '#15171b');
|
|
|
|
ctx.fillStyle = gloveGrad;
|
|
ctx.beginPath();
|
|
ctx.roundRect(-24, -20, 85, 80, 8);
|
|
ctx.fill();
|
|
|
|
// Carbon Fiber Reinforced Knuckle Pad
|
|
ctx.fillStyle = '#0f1114';
|
|
ctx.beginPath();
|
|
ctx.roundRect(-16, -16, 70, 24, 6);
|
|
ctx.fill();
|
|
ctx.strokeStyle = '#444c58';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.stroke();
|
|
|
|
// Gripping Fingers around Weapon
|
|
ctx.fillStyle = '#22262e';
|
|
for (let i = 0; i < gripFingers; i++) {
|
|
ctx.beginPath();
|
|
ctx.roundRect(-20 + i * 18, -48, 16, 36, 6);
|
|
ctx.fill();
|
|
// Finger knuckle highlight
|
|
ctx.fillStyle = '#3a404c';
|
|
ctx.fillRect(-18 + i * 18, -40, 12, 6);
|
|
ctx.fillStyle = '#22262e';
|
|
}
|
|
|
|
// Thumb wrapping over
|
|
ctx.fillStyle = '#1c1f26';
|
|
ctx.beginPath();
|
|
ctx.roundRect(42, -35, 22, 45, 8);
|
|
ctx.fill();
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// WEAPON DRAWING ROUTINES
|
|
// ----------------------------------------------------
|
|
|
|
drawCombatKnife(ctx) {
|
|
ctx.save();
|
|
|
|
let slashProg = Math.sin(this.knifeSlashTimer * Math.PI);
|
|
let slashX = -slashProg * 140;
|
|
let slashY = -slashProg * 80;
|
|
let slashRot = -slashProg * 0.6;
|
|
|
|
ctx.translate(slashX, slashY);
|
|
ctx.rotate(slashRot);
|
|
|
|
// Knife Blade (Titanium Black with Razor Silver Edge)
|
|
ctx.fillStyle = '#181b20';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-10, -260);
|
|
ctx.lineTo(25, -230);
|
|
ctx.lineTo(20, -70);
|
|
ctx.lineTo(-10, -70);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Serrated Spine
|
|
ctx.strokeStyle = '#485260';
|
|
ctx.lineWidth = 3;
|
|
ctx.stroke();
|
|
|
|
// Razor Silver Edge
|
|
ctx.fillStyle = '#dce4ec';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-10, -260);
|
|
ctx.lineTo(0, -250);
|
|
ctx.lineTo(-5, -70);
|
|
ctx.lineTo(-10, -70);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Blood Groove (Fuller)
|
|
ctx.fillStyle = '#0a0c0e';
|
|
ctx.fillRect(4, -200, 5, 110);
|
|
|
|
// Steel Crossguard
|
|
ctx.fillStyle = '#303640';
|
|
ctx.fillRect(-22, -72, 50, 14);
|
|
|
|
// Textured Grip
|
|
ctx.fillStyle = '#111316';
|
|
ctx.fillRect(-14, -58, 34, 90);
|
|
|
|
// Hand holding the knife
|
|
this.drawMarineHand(ctx, -5, -10, 0.1, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawPistolViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Slide Blowback Kick
|
|
const slideY = this.slideOffset * 22;
|
|
|
|
// Gun Frame & Barrel (Brushed Dark Gunmetal)
|
|
ctx.fillStyle = '#1c1e22';
|
|
ctx.fillRect(-35, -210, 55, 160);
|
|
|
|
// Moving Slide
|
|
const slideGrad = ctx.createLinearGradient(-35, -220, 20, -220);
|
|
slideGrad.addColorStop(0, '#3a3f48');
|
|
slideGrad.addColorStop(0.5, '#565e6c');
|
|
slideGrad.addColorStop(1, '#252930');
|
|
|
|
ctx.fillStyle = slideGrad;
|
|
ctx.beginPath();
|
|
ctx.roundRect(-36, -225 + slideY, 58, 140, 4);
|
|
ctx.fill();
|
|
|
|
// Slide Rear Serrations
|
|
ctx.fillStyle = '#15171b';
|
|
for (let i = 0; i < 6; i++) {
|
|
ctx.fillRect(-32, -125 + slideY + i * 6, 50, 3);
|
|
}
|
|
|
|
// Ejection Port
|
|
ctx.fillStyle = '#0a0b0d';
|
|
ctx.fillRect(-8, -170 + slideY, 26, 35);
|
|
if (this.slideOffset > 0.3) {
|
|
// Shiny Brass Bullet in chamber
|
|
ctx.fillStyle = '#e6b800';
|
|
ctx.fillRect(-4, -162 + slideY, 16, 20);
|
|
}
|
|
|
|
// Tactical Rail & Under-Barrel
|
|
ctx.fillStyle = '#16181c';
|
|
ctx.fillRect(-30, -230, 44, 25);
|
|
|
|
// Tritium 3-Dot Sights (Green glowing front & rear dots)
|
|
ctx.fillStyle = '#111';
|
|
ctx.fillRect(-12, -238 + slideY, 10, 12); // Front sight post
|
|
ctx.fillStyle = '#00ff44';
|
|
ctx.fillRect(-10, -236 + slideY, 6, 6);
|
|
|
|
ctx.fillStyle = '#111';
|
|
ctx.fillRect(-34, -98 + slideY, 12, 10); // Rear sight left
|
|
ctx.fillRect(8, -98 + slideY, 12, 10); // Rear sight right
|
|
ctx.fillStyle = '#00ff44';
|
|
ctx.fillRect(-32, -96 + slideY, 6, 6);
|
|
ctx.fillRect(10, -96 + slideY, 6, 6);
|
|
|
|
// Ergonomic Pistol Grip
|
|
ctx.fillStyle = '#111316';
|
|
ctx.beginPath();
|
|
ctx.moveTo(-30, -70);
|
|
ctx.lineTo(-45, 80);
|
|
ctx.lineTo(25, 95);
|
|
ctx.lineTo(15, -70);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Grip Checkering & Doom UAC Insignia
|
|
ctx.fillStyle = '#22252c';
|
|
ctx.fillRect(-32, -30, 38, 70);
|
|
ctx.fillStyle = '#00ff66';
|
|
ctx.fillRect(-18, 0, 10, 10);
|
|
|
|
// Marine Hand gripping pistol
|
|
this.drawMarineHand(ctx, -15, 0, -0.05, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawShotgunViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Long Double/Single Ribbed Barrel (Dark Steel)
|
|
const barrelGrad = ctx.createLinearGradient(-40, -320, 30, -320);
|
|
barrelGrad.addColorStop(0, '#2a2e36');
|
|
barrelGrad.addColorStop(0.5, '#4f5664');
|
|
barrelGrad.addColorStop(1, '#1b1d22');
|
|
|
|
ctx.fillStyle = barrelGrad;
|
|
ctx.fillRect(-28, -320, 46, 260);
|
|
|
|
// Ventilated Top Heat Shield Rib
|
|
ctx.fillStyle = '#16181d';
|
|
ctx.fillRect(-12, -330, 14, 270);
|
|
ctx.fillStyle = '#ffaa00';
|
|
ctx.beginPath();
|
|
ctx.arc(-5, -330, 4, 0, Math.PI * 2); // Brass bead sight
|
|
ctx.fill();
|
|
|
|
// Muzzle Bore
|
|
ctx.fillStyle = '#08090a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(-5, -320, 16, 12, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Pump-Action Forend Slide (Sliding Back and Forth on pumpProgress)
|
|
const pumpY = Math.sin(this.pumpProgress * Math.PI) * 45;
|
|
|
|
// Wood / Heavy Polymer Pump Grip
|
|
const pumpGrad = ctx.createLinearGradient(-36, -200 + pumpY, 26, -200 + pumpY);
|
|
pumpGrad.addColorStop(0, '#663a1e');
|
|
pumpGrad.addColorStop(0.5, '#8c522e');
|
|
pumpGrad.addColorStop(1, '#4a2712');
|
|
|
|
ctx.fillStyle = pumpGrad;
|
|
ctx.beginPath();
|
|
ctx.roundRect(-34, -220 + pumpY, 58, 85, 8);
|
|
ctx.fill();
|
|
|
|
// Grooves on Pump Grip
|
|
ctx.fillStyle = '#2d1608';
|
|
for (let i = 0; i < 7; i++) {
|
|
ctx.fillRect(-32, -210 + pumpY + i * 10, 54, 3);
|
|
}
|
|
|
|
// Heavy Receiver Body
|
|
ctx.fillStyle = '#1c1f26';
|
|
ctx.fillRect(-35, -70, 62, 110);
|
|
ctx.fillStyle = '#0d0e12';
|
|
ctx.fillRect(-10, -50, 32, 25); // Ejection port
|
|
|
|
// Left Supporting Hand on Pump Slide
|
|
ctx.save();
|
|
ctx.translate(-15, -170 + pumpY);
|
|
ctx.fillStyle = '#1c1f26';
|
|
ctx.beginPath();
|
|
ctx.roundRect(-30, -15, 60, 35, 6);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
// Right Main Hand on Receiver Grip
|
|
this.drawMarineHand(ctx, -10, 20, -0.08, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawChaingunViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Heavy Motorized Housing
|
|
ctx.fillStyle = '#1a1d22';
|
|
ctx.fillRect(-50, -120, 95, 160);
|
|
|
|
// Rotary Gear Housing
|
|
ctx.fillStyle = '#2b303a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(-5, -120, 52, 38, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// 6 Multi-Barrels Bundle with 3D Rotation Animation
|
|
for (let i = 0; i < 6; i++) {
|
|
const angle = this.gatlingAngle + (i * Math.PI) / 3;
|
|
const bx = -5 + Math.cos(angle) * 32;
|
|
const by = -120 + Math.sin(angle) * 22;
|
|
|
|
const bGrad = ctx.createLinearGradient(bx - 10, by - 200, bx + 10, by - 200);
|
|
bGrad.addColorStop(0, '#383e4a');
|
|
bGrad.addColorStop(0.5, '#687388');
|
|
bGrad.addColorStop(1, '#1e2229');
|
|
|
|
ctx.fillStyle = bGrad;
|
|
ctx.fillRect(bx - 8, by - 190, 16, 190);
|
|
|
|
// Muzzle bore
|
|
ctx.fillStyle = '#0a0c0e';
|
|
ctx.beginPath();
|
|
ctx.ellipse(bx, by - 190, 6, 4, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Heavy Ammo Feed Chute with Golden Brass Belt Links
|
|
ctx.fillStyle = '#d4af37';
|
|
for (let i = 0; i < 5; i++) {
|
|
ctx.fillRect(-70 - i * 10, -60 + i * 14, 18, 12);
|
|
ctx.fillStyle = '#8a7018';
|
|
ctx.fillRect(-68 - i * 10, -58 + i * 14, 14, 8);
|
|
ctx.fillStyle = '#d4af37';
|
|
}
|
|
|
|
// Digital Ammo Counter on Receiver
|
|
ctx.fillStyle = '#050a06';
|
|
ctx.fillRect(-35, -95, 36, 20);
|
|
ctx.fillStyle = '#00ff44';
|
|
ctx.font = 'bold 13px monospace';
|
|
ctx.fillText(this.player.ammo.bullets, -30, -80);
|
|
|
|
// Marine Gloved Hands (Side support grip + rear trigger grip)
|
|
this.drawMarineHand(ctx, -15, 30, -0.05, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawRocketViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Large Shoulder-Mounted Launcher Tube (Olive Drab)
|
|
const tubeGrad = ctx.createLinearGradient(-60, -300, 40, -300);
|
|
tubeGrad.addColorStop(0, '#36442e');
|
|
tubeGrad.addColorStop(0.5, '#566b49');
|
|
tubeGrad.addColorStop(1, '#232c1e');
|
|
|
|
ctx.fillStyle = tubeGrad;
|
|
ctx.fillRect(-55, -310, 95, 360);
|
|
|
|
// Heavy Hazard Caution Stripes
|
|
ctx.fillStyle = '#e6b800';
|
|
ctx.fillRect(-55, -270, 95, 24);
|
|
ctx.fillStyle = '#111';
|
|
for (let i = 0; i < 4; i++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(-50 + i * 24, -270);
|
|
ctx.lineTo(-38 + i * 24, -246);
|
|
ctx.lineTo(-46 + i * 24, -246);
|
|
ctx.lineTo(-58 + i * 24, -270);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Huge Launcher Bore
|
|
ctx.fillStyle = '#0a0d08';
|
|
ctx.beginPath();
|
|
ctx.ellipse(-8, -310, 44, 28, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Olive Missile Warhead Tip visible inside
|
|
ctx.fillStyle = '#405236';
|
|
ctx.beginPath();
|
|
ctx.arc(-8, -305, 24, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.fillStyle = '#ff2200';
|
|
ctx.fillRect(-11, -318, 6, 8); // Detonator fuse tip
|
|
|
|
// Tactical Sights & Grips
|
|
ctx.fillStyle = '#16181b';
|
|
ctx.fillRect(-68, -220, 14, 45); // Side scope
|
|
ctx.fillStyle = '#ff0033';
|
|
ctx.beginPath();
|
|
ctx.arc(-61, -198, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Marine Hands holding shoulder launcher
|
|
this.drawMarineHand(ctx, -15, 30, -0.05, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawPlasmaViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Futuristic Carbine Chassis (Matte Carbon & Titanium)
|
|
const frameGrad = ctx.createLinearGradient(-45, -260, 35, -260);
|
|
frameGrad.addColorStop(0, '#1c222a');
|
|
frameGrad.addColorStop(0.5, '#354050');
|
|
frameGrad.addColorStop(1, '#14181e');
|
|
|
|
ctx.fillStyle = frameGrad;
|
|
ctx.fillRect(-40, -280, 72, 300);
|
|
|
|
// Glowing Blue Plasma Cooling Coils
|
|
const pulse = 0.7 + Math.sin(performance.now() * 0.008) * 0.3;
|
|
for (let i = 0; i < 4; i++) {
|
|
ctx.fillStyle = `rgba(0, 212, 255, ${pulse})`;
|
|
ctx.beginPath();
|
|
ctx.roundRect(-32, -240 + i * 32, 56, 18, 6);
|
|
ctx.fill();
|
|
|
|
// Core electric wire
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(-24, -234 + i * 32, 40, 6);
|
|
}
|
|
|
|
// Dual Plasma Muzzle Emitters
|
|
ctx.fillStyle = '#0a1016';
|
|
ctx.fillRect(-36, -300, 20, 25);
|
|
ctx.fillRect(8, -300, 20, 25);
|
|
ctx.fillStyle = '#00d4ff';
|
|
ctx.fillRect(-30, -305, 8, 8);
|
|
ctx.fillRect(14, -305, 8, 8);
|
|
|
|
// Marine Hand
|
|
this.drawMarineHand(ctx, -10, 10, -0.06, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawBFGViewmodel(ctx) {
|
|
ctx.save();
|
|
|
|
// Massive Heavy Weapon Body (Dark Iron & Heavy Armor)
|
|
const bfgGrad = ctx.createLinearGradient(-80, -280, 60, -280);
|
|
bfgGrad.addColorStop(0, '#152016');
|
|
bfgGrad.addColorStop(0.5, '#2e4230');
|
|
bfgGrad.addColorStop(1, '#0e160e');
|
|
|
|
ctx.fillStyle = bfgGrad;
|
|
ctx.fillRect(-75, -290, 135, 320);
|
|
|
|
// Giant Radioactive Containment Sphere
|
|
const chargePulse = this.bfgCharging ? (1.0 - this.bfgChargeTimer / 0.6) : 0.4;
|
|
const sphereGrad = ctx.createRadialGradient(-8, -160, 10, -8, -160, 55);
|
|
sphereGrad.addColorStop(0, '#ffffff');
|
|
sphereGrad.addColorStop(0.4, `rgba(0, 255, 68, ${0.8 + chargePulse * 0.2})`);
|
|
sphereGrad.addColorStop(1, 'rgba(0, 60, 15, 0.9)');
|
|
|
|
ctx.fillStyle = sphereGrad;
|
|
ctx.beginPath();
|
|
ctx.arc(-8, -160, 52, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Biohazard Warning Logo
|
|
ctx.fillStyle = '#00ff44';
|
|
ctx.font = 'bold 16px monospace';
|
|
ctx.fillText('⚡ BFG 9000 ⚡', -65, -80);
|
|
|
|
// Colossal Muzzle Emitter Hood
|
|
ctx.fillStyle = '#080d08';
|
|
ctx.fillRect(-65, -315, 115, 30);
|
|
ctx.strokeStyle = '#00ff44';
|
|
ctx.lineWidth = 4;
|
|
ctx.strokeRect(-65, -315, 115, 30);
|
|
|
|
// Marine Hands gripping heavy side handles
|
|
this.drawMarineHand(ctx, -20, 20, -0.05, 4);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
drawMuzzleFlashBurst(ctx, weaponId) {
|
|
const flashX = -8;
|
|
let flashY = -240;
|
|
let flashRadius = 90;
|
|
|
|
if (weaponId === 'pistol') {
|
|
flashY = -250;
|
|
flashRadius = 75;
|
|
} else if (weaponId === 'shotgun') {
|
|
flashY = -345;
|
|
flashRadius = 110;
|
|
} else if (weaponId === 'chaingun') {
|
|
flashY = -215;
|
|
flashRadius = 85;
|
|
} else if (weaponId === 'rocket') {
|
|
flashY = -330;
|
|
flashRadius = 130;
|
|
} else if (weaponId === 'plasma') {
|
|
flashY = -315;
|
|
flashRadius = 95;
|
|
} else if (weaponId === 'bfg') {
|
|
flashY = -330;
|
|
flashRadius = 160;
|
|
}
|
|
|
|
const grad = ctx.createRadialGradient(flashX, flashY, 15, flashX, flashY, flashRadius);
|
|
|
|
if (weaponId === 'plasma') {
|
|
grad.addColorStop(0, '#ffffff');
|
|
grad.addColorStop(0.3, '#00d4ff');
|
|
grad.addColorStop(0.7, '#0055ff');
|
|
grad.addColorStop(1, 'transparent');
|
|
} else if (weaponId === 'bfg') {
|
|
grad.addColorStop(0, '#ffffff');
|
|
grad.addColorStop(0.3, '#00ff44');
|
|
grad.addColorStop(0.7, '#006611');
|
|
grad.addColorStop(1, 'transparent');
|
|
} else {
|
|
grad.addColorStop(0, '#ffffff');
|
|
grad.addColorStop(0.3, '#ffea00');
|
|
grad.addColorStop(0.7, '#ff4400');
|
|
grad.addColorStop(1, 'transparent');
|
|
}
|
|
|
|
ctx.fillStyle = grad;
|
|
ctx.beginPath();
|
|
ctx.arc(flashX, flashY, flashRadius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Fiery Muzzle Sparks Spike Stars
|
|
ctx.fillStyle = '#ffffff';
|
|
for (let i = 0; i < 8; i++) {
|
|
const angle = (i * Math.PI) / 4 + Math.random() * 0.2;
|
|
const len = flashRadius * (0.8 + Math.random() * 0.5);
|
|
ctx.beginPath();
|
|
ctx.moveTo(flashX, flashY);
|
|
ctx.lineTo(flashX + Math.cos(angle - 0.1) * 8, flashY + Math.sin(angle - 0.1) * 8);
|
|
ctx.lineTo(flashX + Math.cos(angle) * len, flashY + Math.sin(angle) * len);
|
|
ctx.lineTo(flashX + Math.cos(angle + 0.1) * 8, flashY + Math.sin(angle + 0.1) * 8);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
renderCasings(ctx, originX, originY) {
|
|
for (let i = 0; i < this.ejectedCasings.length; i++) {
|
|
const c = this.ejectedCasings[i];
|
|
ctx.save();
|
|
ctx.translate(originX + c.x, originY + c.y);
|
|
ctx.rotate(c.rot);
|
|
|
|
if (c.type === 'shell') {
|
|
// Red Shotgun Shell with Brass Rim
|
|
ctx.fillStyle = '#cc1100';
|
|
ctx.fillRect(-10, -4, 20, 8);
|
|
ctx.fillStyle = '#e6b800';
|
|
ctx.fillRect(-14, -4, 5, 8);
|
|
} else {
|
|
// Golden Brass Bullet Casing
|
|
ctx.fillStyle = '#d4af37';
|
|
ctx.fillRect(-8, -3, 16, 6);
|
|
ctx.fillStyle = '#8c7018';
|
|
ctx.fillRect(-10, -3, 3, 6);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
}
|
|
|
|
window.DoomWeapons = DoomWeapons;
|