Files
ai-qpid-pvz/game/doom/js/weapons.js
T

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;