690 lines
22 KiB
JavaScript
Executable File
690 lines
22 KiB
JavaScript
Executable File
/**
|
|
* 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;
|