/** * DOOM 3D - Enemies & Monster AI System * 5 Iconic Doom Demons: Zombieman, Shotgunner, Imp, Pinky Demon, Cacodemon. * 3D Billboards, multi-state animations, line-of-sight tracking, attacks, and blood FX. */ class DoomEnemies { constructor(engine, player, sound, map) { this.engine = engine; this.player = player; this.sound = sound; this.map = map; this.enemies = []; this.monsterProjectiles = []; this.textures = {}; this.generateMonsterTextures(); this.spawnMapEnemies(); } // ---------------------------------------------------- // PROCEDURAL HIGH-DEFINITION RETRO SPRITE TEXTURES // ---------------------------------------------------- generateMonsterTextures() { this.textures.zombie = this.createZombieSprites(); this.textures.shotgunner = this.createShotgunnerSprites(); this.textures.imp = this.createImpSprites(); this.textures.pinky = this.createPinkySprites(); this.textures.caco = this.createCacoSprites(); } createZombieSprites() { return { walk1: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 1), walk2: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ff3300', 'rifle', 2), attack: this.drawHumanoidSprite('#3d6b2c', '#a89078', '#ffea00', 'rifle_fire', 1), pain: this.drawHumanoidSprite('#882222', '#c06060', '#ffffff', 'rifle', 1), dead: this.drawDeadCorpseSprite('#3d6b2c', '#990000') }; } createShotgunnerSprites() { return { walk1: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 1), walk2: this.drawHumanoidSprite('#22252a', '#9098a0', '#ff0000', 'shotgun', 2), attack: this.drawHumanoidSprite('#22252a', '#9098a0', '#ffaa00', 'shotgun_fire', 1), pain: this.drawHumanoidSprite('#6b1a1a', '#aa4444', '#ffffff', 'shotgun', 1), dead: this.drawDeadCorpseSprite('#22252a', '#bb0000') }; } createImpSprites() { return { walk1: this.drawImpSprite(1), walk2: this.drawImpSprite(2), attack: this.drawImpSprite(3), // casting fireball pain: this.drawImpSprite(4), dead: this.drawDeadCorpseSprite('#6e4428', '#aa1100') }; } createPinkySprites() { return { walk1: this.drawPinkySprite(1), walk2: this.drawPinkySprite(2), attack: this.drawPinkySprite(3), // wide jaws bite pain: this.drawPinkySprite(4), dead: this.drawDeadCorpseSprite('#a82838', '#880000') }; } createCacoSprites() { return { walk1: this.drawCacoSprite(1), walk2: this.drawCacoSprite(2), attack: this.drawCacoSprite(3), // blue lightning maw pain: this.drawCacoSprite(4), dead: this.drawDeadCorpseSprite('#c41c1c', '#00d4ff') }; } drawHumanoidSprite(armorColor, skinColor, eyeColor, weaponType, frame) { const c = document.createElement('canvas'); c.width = 160; c.height = 180; const ctx = c.getContext('2d'); // Shadow under feet ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.ellipse(80, 165, 38, 12, 0, 0, Math.PI * 2); ctx.fill(); // Boots / Legs ctx.fillStyle = '#181b16'; if (frame === 1) { ctx.fillRect(58, 105, 20, 58); ctx.fillRect(82, 105, 20, 58); } else { ctx.fillRect(48, 105, 22, 58); ctx.fillRect(90, 105, 22, 58); } // Torso / Tactical Armor Vest ctx.fillStyle = armorColor; ctx.fillRect(50, 48, 60, 60); ctx.fillStyle = '#1c201a'; ctx.fillRect(56, 54, 48, 18); // Chest plate detail ctx.fillRect(54, 94, 52, 10); // Utility belt // Muscular / Zombie Flesh Head ctx.fillStyle = skinColor; ctx.fillRect(62, 16, 36, 34); // Glowing Demonic Eyes ctx.fillStyle = eyeColor; ctx.fillRect(68, 26, 8, 7); ctx.fillRect(84, 26, 8, 7); // Sinister Mouth ctx.fillStyle = '#330000'; ctx.fillRect(72, 38, 16, 6); // Zombie Helmet / Hair ctx.fillStyle = '#33382e'; ctx.fillRect(58, 10, 44, 12); // Weapon if (weaponType.includes('rifle')) { ctx.fillStyle = '#111316'; ctx.fillRect(75, 62, 60, 14); ctx.fillStyle = '#444c54'; ctx.fillRect(82, 58, 24, 8); if (weaponType === 'rifle_fire') { ctx.fillStyle = '#ffea00'; ctx.beginPath(); ctx.arc(142, 69, 20, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(142, 69, 10, 0, Math.PI * 2); ctx.fill(); } } else if (weaponType.includes('shotgun')) { ctx.fillStyle = '#1a1c20'; ctx.fillRect(70, 64, 70, 18); ctx.fillStyle = '#7a3e1d'; // Wooden pump ctx.fillRect(92, 70, 24, 14); if (weaponType === 'shotgun_fire') { ctx.fillStyle = '#ffaa00'; ctx.beginPath(); ctx.arc(148, 73, 26, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(148, 73, 14, 0, Math.PI * 2); ctx.fill(); } } const tex = new THREE.CanvasTexture(c); tex.magFilter = THREE.NearestFilter; return tex; } drawImpSprite(pose) { const c = document.createElement('canvas'); c.width = 160; c.height = 180; const ctx = c.getContext('2d'); // Shadow ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.ellipse(80, 165, 42, 12, 0, 0, Math.PI * 2); ctx.fill(); // Brown Spiky Demon Body ctx.fillStyle = pose === 4 ? '#8a2d1d' : '#6b4124'; // Torso ctx.fillRect(52, 45, 56, 62); // Demonic Spikes on shoulders & head ctx.fillStyle = '#e8d8b8'; ctx.fillRect(42, 38, 12, 14); ctx.fillRect(106, 38, 12, 14); ctx.fillRect(56, 8, 10, 18); ctx.fillRect(94, 8, 10, 18); // Head ctx.fillStyle = pose === 4 ? '#9c3622' : '#7c4d2d'; ctx.fillRect(58, 18, 44, 35); // Glowing Fiery Red Eyes ctx.fillStyle = '#ff0000'; ctx.fillRect(66, 28, 8, 8); ctx.fillRect(86, 28, 8, 8); ctx.fillStyle = '#ffff00'; ctx.fillRect(68, 30, 4, 4); ctx.fillRect(88, 30, 4, 4); // Sharp Demonic Fangs ctx.fillStyle = '#ffffff'; for (let i = 0; i < 4; i++) { ctx.fillRect(68 + i * 7, 42, 4, 6); } // Legs with clawed hooves ctx.fillStyle = '#523018'; if (pose === 1) { ctx.fillRect(52, 107, 22, 56); ctx.fillRect(86, 107, 22, 56); } else if (pose === 2) { ctx.fillRect(44, 107, 22, 56); ctx.fillRect(94, 107, 22, 56); } else { ctx.fillRect(50, 107, 24, 56); ctx.fillRect(86, 107, 24, 56); } // Fireball casting pose if (pose === 3) { // Glowing Fireball in hands ctx.fillStyle = '#ff5500'; ctx.beginPath(); ctx.arc(125, 75, 24, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#ffff00'; ctx.beginPath(); ctx.arc(125, 75, 14, 0, Math.PI * 2); ctx.fill(); } const tex = new THREE.CanvasTexture(c); tex.magFilter = THREE.NearestFilter; return tex; } drawPinkySprite(frame) { const c = document.createElement('canvas'); c.width = 180; c.height = 180; const ctx = c.getContext('2d'); // Shadow ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.beginPath(); ctx.ellipse(90, 165, 52, 14, 0, 0, Math.PI * 2); ctx.fill(); // Massive Demonic Muscle Beast (Pinky) ctx.fillStyle = frame === 4 ? '#781822' : '#b82e44'; ctx.fillRect(40, 40, 100, 70); // Demon Horns ctx.fillStyle = '#e6d8c8'; ctx.fillRect(28, 22, 18, 24); ctx.fillRect(134, 22, 18, 24); // Huge Demon Jaws & Snout ctx.fillStyle = '#8a1d2e'; ctx.fillRect(55, 50, 70, 55); // Glowing Yellow Eyes ctx.fillStyle = '#ffff33'; ctx.fillRect(60, 55, 12, 10); ctx.fillRect(108, 55, 12, 10); // Terrifying Sharp Teeth ctx.fillStyle = '#ffffff'; for (let i = 0; i < 6; i++) { ctx.fillRect(62 + i * 10, 85, 6, 12); if (frame === 3) { // Open jaw ctx.fillRect(62 + i * 10, 70, 6, 8); } } // Heavy Stomping Legs ctx.fillStyle = '#6e1422'; if (frame === 1) { ctx.fillRect(42, 110, 32, 54); ctx.fillRect(106, 110, 32, 54); } else { ctx.fillRect(30, 110, 32, 54); ctx.fillRect(118, 110, 32, 54); } const tex = new THREE.CanvasTexture(c); tex.magFilter = THREE.NearestFilter; return tex; } drawCacoSprite(frame) { const c = document.createElement('canvas'); c.width = 180; c.height = 180; const ctx = c.getContext('2d'); // Giant Red Floating Sphere Demon ctx.fillStyle = '#c41c1c'; ctx.beginPath(); ctx.arc(90, 90, 72, 0, Math.PI * 2); ctx.fill(); // Spikes on head ctx.fillStyle = '#6e0c0c'; for (let i = 0; i < 8; i++) { const angle = Math.PI + (i * Math.PI) / 7; const hx = 90 + Math.cos(angle) * 70; const hy = 90 + Math.sin(angle) * 70; ctx.fillRect(hx - 6, hy - 6, 12, 12); } // Massive Emerald Eye ctx.fillStyle = '#00ff55'; ctx.beginPath(); ctx.arc(90, 65, 24, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#003311'; ctx.beginPath(); ctx.arc(90, 65, 10, 0, Math.PI * 2); ctx.fill(); // Gaping Maw ctx.fillStyle = '#111116'; ctx.fillRect(50, 105, 80, 34); if (frame === 3) { // Blue Lightning Ball inside mouth ctx.fillStyle = '#00d4ff'; ctx.beginPath(); ctx.arc(90, 122, 22, 0, Math.PI * 2); ctx.fill(); } else { // Teeth ctx.fillStyle = '#ffffff'; for (let i = 0; i < 7; i++) { ctx.fillRect(55 + i * 10, 105, 6, 10); ctx.fillRect(55 + i * 10, 128, 6, 10); } } const tex = new THREE.CanvasTexture(c); tex.magFilter = THREE.NearestFilter; return tex; } drawDeadCorpseSprite(bodyColor, bloodColor) { const c = document.createElement('canvas'); c.width = 160; c.height = 90; const ctx = c.getContext('2d'); // Blood Pool on Floor ctx.fillStyle = bloodColor; ctx.beginPath(); ctx.ellipse(80, 58, 68, 24, 0, 0, Math.PI * 2); ctx.fill(); // Collapsed Demonic Body ctx.fillStyle = bodyColor; ctx.fillRect(35, 36, 95, 28); // Skull / Slain Head ctx.fillStyle = '#d0a080'; ctx.fillRect(20, 42, 20, 18); const tex = new THREE.CanvasTexture(c); tex.magFilter = THREE.NearestFilter; return tex; } // ---------------------------------------------------- // SPAWNING ENEMIES // ---------------------------------------------------- spawnMapEnemies() { const spawns = (this.map && this.map.spawns) ? this.map.spawns.enemies : (window.game && window.game.map ? window.game.map.spawns.enemies : []); if (!spawns || spawns.length === 0) return; for (let i = 0; i < spawns.length; i++) { const s = spawns[i]; this.createEnemy(s.type, s.x, s.y, s.z); } } createEnemy(type, x, y, z) { let maxHp = 30; let speed = 4.5; let scaleW = 2.4; let scaleH = 2.6; let attackRange = 28; let isFlying = false; if (type === 'zombie') { maxHp = 30; speed = 3.6; attackRange = 30; } else if (type === 'shotgunner') { maxHp = 45; speed = 4.0; attackRange = 24; } else if (type === 'imp') { maxHp = 65; speed = 4.6; attackRange = 35; } else if (type === 'pinky') { maxHp = 140; speed = 7.5; attackRange = 2.8; scaleW = 2.8; scaleH = 2.8; } else if (type === 'caco') { maxHp = 220; speed = 4.0; attackRange = 35; scaleW = 3.4; scaleH = 3.4; isFlying = true; } // Sprite Material - depthTest true, depthWrite false for proper transparency const spriteMat = new THREE.SpriteMaterial({ map: this.textures[type].walk1, transparent: true, depthTest: true, depthWrite: false }); const sprite = new THREE.Sprite(spriteMat); const spawnY = isFlying ? (y + 3.5) : (y + scaleH / 2); sprite.position.set(x, spawnY, z); sprite.scale.set(scaleW, scaleH, 1); this.engine.scene.add(sprite); const enemyObj = { type, mesh: sprite, material: spriteMat, hp: maxHp, maxHp: maxHp, speed: speed, scaleW: scaleW, scaleH: scaleH, radius: scaleW * 0.45, attackRange: attackRange, isFlying: isFlying, state: 'idle', stateTimer: 0, animTimer: 0, animFrame: 1, attackCooldown: 0.8 + Math.random() * 0.8, isDead: false, takeDamage: (dmg, hitPos) => this.damageEnemy(enemyObj, dmg, hitPos) }; this.enemies.push(enemyObj); return enemyObj; } damageEnemy(e, amount, hitPos) { if (e.isDead) return; e.hp -= amount; if (this.sound) this.sound.playEnemyPain(); // Alert enemy and nearby monsters! e.state = 'chase'; this.alertNearbyMonsters(e.mesh.position, 35); if (e.hp <= 0) { this.killEnemy(e); } else { // Pain stagger frame e.state = 'pain'; e.stateTimer = 0.22; e.material.map = this.textures[e.type].pain; } } killEnemy(e) { e.isDead = true; e.state = 'dead'; e.mesh.scale.set(e.scaleW * 1.3, e.scaleH * 0.5, 1); if (!e.isFlying) { e.mesh.position.y = Math.max(0.1, e.mesh.position.y - e.scaleH * 0.35); } e.material.map = this.textures[e.type].dead; if (this.sound) this.sound.playEnemyDeath(); // Drop loot if (e.type === 'shotgunner') { if (window.game && window.game.items) { window.game.items.spawnItem('shotgun', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z); } } else if (e.type === 'zombie') { if (window.game && window.game.items && Math.random() > 0.4) { window.game.items.spawnItem('ammo_clip', e.mesh.position.x, e.mesh.position.y, e.mesh.position.z); } } } alertNearbyMonsters(pos, radius) { for (let i = 0; i < this.enemies.length; i++) { const other = this.enemies[i]; if (!other.isDead && other.state === 'idle') { if (other.mesh.position.distanceTo(pos) < radius) { other.state = 'chase'; if (this.sound) this.sound.playEnemyAlert(other.type); } } } } // ---------------------------------------------------- // ENEMY AI & PROJECTILES UPDATE LOOP // ---------------------------------------------------- update(delta) { const playerPos = this.player.position; // 1. Update Demon AI for (let i = 0; i < this.enemies.length; i++) { const e = this.enemies[i]; if (e.isDead) continue; const dist = e.mesh.position.distanceTo(playerPos); // Alert enemy if player is close or in line of sight if (e.state === 'idle' && dist < 28) { e.state = 'chase'; if (this.sound) this.sound.playEnemyAlert(e.type); } // State Handling if (e.state === 'pain') { e.stateTimer -= delta; if (e.stateTimer <= 0) { e.state = 'chase'; } } else if (e.state === 'attack') { e.stateTimer -= delta; if (e.stateTimer <= 0) { e.state = 'chase'; e.attackCooldown = 1.2 + Math.random() * 0.8; } } else if (e.state === 'chase') { e.attackCooldown -= delta; // Move towards player const dir = new THREE.Vector3().subVectors(playerPos, e.mesh.position); if (!e.isFlying) dir.y = 0; dir.normalize(); // Check collision with other enemies to prevent stacking const moveDist = e.speed * delta; e.mesh.position.addScaledVector(dir, moveDist); // Animation walking frames e.animTimer += delta * 6; if (e.animTimer >= 1.0) { e.animTimer = 0; e.animFrame = e.animFrame === 1 ? 2 : 1; e.material.map = (e.animFrame === 1) ? this.textures[e.type].walk1 : this.textures[e.type].walk2; } // Attack Player if within range and cooldown is ready if (dist <= e.attackRange && e.attackCooldown <= 0) { this.triggerEnemyAttack(e); } } } // 2. Update Monster Flying Projectiles (Fireballs & Plasma balls) for (let i = this.monsterProjectiles.length - 1; i >= 0; i--) { const p = this.monsterProjectiles[i]; p.mesh.position.addScaledVector(p.velocity, delta); p.light.position.copy(p.mesh.position); p.life -= delta; // Check hit against player const pDist = p.mesh.position.distanceTo(this.player.position); if (pDist < 1.4) { this.player.takeDamage(p.damage, p.mesh.position); this.engine.createExplosionFX(p.mesh.position, p.isCaco ? 0x00d4ff : 0xff5500); this.destroyMonsterProjectile(i); continue; } if (p.life <= 0) { this.destroyMonsterProjectile(i); } } } triggerEnemyAttack(e) { e.state = 'attack'; e.stateTimer = 0.5; e.material.map = this.textures[e.type].attack; const playerPos = this.player.position; if (e.type === 'pinky') { // Melee Bite Attack if (e.mesh.position.distanceTo(playerPos) < 3.2) { this.player.takeDamage(18, e.mesh.position); } } else if (e.type === 'zombie') { // Hitscan rifle shot if (this.sound) this.sound.playPistol(); this.engine.createMuzzleLight(e.mesh.position, 0xffcc00); this.player.takeDamage(8, e.mesh.position); } else if (e.type === 'shotgunner') { // Hitscan shotgun blast if (this.sound) this.sound.playShotgun(); this.engine.createMuzzleLight(e.mesh.position, 0xff9900); this.player.takeDamage(18, e.mesh.position); } else if (e.type === 'imp') { // Launch Fireball projectile this.spawnFireball(e.mesh.position, playerPos, 16, 22.0); } else if (e.type === 'caco') { // Launch Blue Lightning Plasma projectile this.spawnCacoBall(e.mesh.position, playerPos, 28, 25.0); } } spawnFireball(from, to, damage, speed) { const geo = new THREE.SphereGeometry(0.35, 8, 8); const mat = new THREE.MeshBasicMaterial({ color: 0xff4400 }); const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(from); this.engine.scene.add(mesh); const light = new THREE.PointLight(0xff5500, 3.0, 10, 2); light.position.copy(from); this.engine.scene.add(light); const target = new THREE.Vector3(to.x, to.y + 1.2, to.z); const dir = new THREE.Vector3().subVectors(target, from).normalize(); this.monsterProjectiles.push({ mesh, light, velocity: dir.multiplyScalar(speed), damage, life: 4.0, isCaco: false }); } spawnCacoBall(from, to, damage, speed) { const geo = new THREE.SphereGeometry(0.45, 8, 8); const mat = new THREE.MeshBasicMaterial({ color: 0x00d4ff }); const mesh = new THREE.Mesh(geo, mat); mesh.position.copy(from); this.engine.scene.add(mesh); const light = new THREE.PointLight(0x00d4ff, 3.5, 12, 2); light.position.copy(from); this.engine.scene.add(light); const target = new THREE.Vector3(to.x, to.y + 1.2, to.z); const dir = new THREE.Vector3().subVectors(target, from).normalize(); this.monsterProjectiles.push({ mesh, light, velocity: dir.multiplyScalar(speed), damage, life: 4.0, isCaco: true }); } destroyMonsterProjectile(index) { const p = this.monsterProjectiles[index]; this.engine.scene.remove(p.mesh); this.engine.scene.remove(p.light); this.monsterProjectiles.splice(index, 1); } } window.DoomEnemies = DoomEnemies;