feat(integrations): add retro doom web engine and agy proxy nginx config
This commit is contained in:
1 parent
b8db5a123d
commit
50eadb4b5e
15 files changed
+6397
No files matched your search
Executable
+689
@@ -0,0 +1,689 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in new issue
Block a user