feat(integrations): add retro doom web engine and agy proxy nginx config

This commit is contained in:
2026-09-22 09:25:23 +00:00
parent b8db5a123d
commit 50eadb4b5e
15 changed files with 6397 additions and 0 deletions
+689
View File
@@ -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;