// DOOM-inspired FPS Game using Three.js and Entity Component System // ============================================================================ // Core Engine and ECS Implementation // ============================================================================ // ECS Core class Component { constructor() { this.entity = null; } init() {} update(dt) {} } class Entity { constructor() { this.id = Entity.nextId++; this.components = new Map(); this.tags = new Set(); } addComponent(componentClass, ...args) { const component = new componentClass(...args); component.entity = this; this.components.set(componentClass, component); component.init(); return component; } getComponent(componentClass) { return this.components.get(componentClass); } hasComponent(componentClass) { return this.components.has(componentClass); } removeComponent(componentClass) { this.components.delete(componentClass); } addTag(tag) { this.tags.add(tag); return this; } hasTag(tag) { return this.tags.has(tag); } destroy() { ECS.removeEntity(this); } } Entity.nextId = 0; // Main ECS singleton const ECS = { entities: [], systems: [], addEntity(entity) { this.entities.push(entity); return entity; }, createEntity() { const entity = new Entity(); return this.addEntity(entity); }, removeEntity(entity) { const index = this.entities.indexOf(entity); if (index !== -1) { this.entities.splice(index, 1); } }, addSystem(system) { this.systems.push(system); system.init(); return system; }, update(dt) { for (const system of this.systems) { system.update(dt); } } }; // System base class class System { constructor() {} init() {} update(dt) {} getEntitiesWith(...componentClasses) { return ECS.entities.filter(entity => { return componentClasses.every(componentClass => entity.hasComponent(componentClass)); }); } } // ============================================================================ // Game Components // ============================================================================ // Transform Component class TransformComponent extends Component { constructor(position = new THREE.Vector3(), rotation = new THREE.Euler(), scale = new THREE.Vector3(1, 1, 1)) { super(); this.position = position; this.rotation = rotation; this.scale = scale; this.matrix = new THREE.Matrix4(); this.updateMatrix(); } updateMatrix() { this.matrix.makeRotationFromEuler(this.rotation); this.matrix.setPosition(this.position); return this.matrix; } moveForward(distance) { const direction = new THREE.Vector3(0, 0, -1).applyEuler(this.rotation); this.position.addScaledVector(direction, distance); } moveRight(distance) { const direction = new THREE.Vector3(1, 0, 0).applyEuler(this.rotation); this.position.addScaledVector(direction, distance); } } // Model Component class ModelComponent extends Component { constructor(mesh) { super(); this.mesh = mesh; } init() { // Update mesh position based on transform const transform = this.entity.getComponent(TransformComponent); if (transform) { this.mesh.position.copy(transform.position); this.mesh.rotation.copy(transform.rotation); this.mesh.scale.copy(transform.scale); } } update() { // Sync mesh with transform const transform = this.entity.getComponent(TransformComponent); if (transform) { this.mesh.position.copy(transform.position); this.mesh.rotation.copy(transform.rotation); this.mesh.scale.copy(transform.scale); } } } // Camera Component class CameraComponent extends Component { constructor(camera) { super(); this.camera = camera; this.offset = new THREE.Vector3(0, 1.7, 0); // Eye height } update() { const transform = this.entity.getComponent(TransformComponent); if (transform) { this.camera.position.copy(transform.position).add(this.offset); this.camera.rotation.copy(transform.rotation); } } } // Physics Component class PhysicsComponent extends Component { constructor(collider, mass = 1) { super(); this.collider = collider; // Represents the collision shape this.velocity = new THREE.Vector3(); this.acceleration = new THREE.Vector3(); this.mass = mass; this.grounded = false; } applyForce(force) { // F = ma, so a = F/m const acceleration = force.clone().divideScalar(this.mass); this.acceleration.add(acceleration); } update(dt) { // v = v₀ + a·t this.velocity.addScaledVector(this.acceleration, dt); // Apply friction when grounded if (this.grounded) { const friction = 0.9; this.velocity.x *= friction; this.velocity.z *= friction; } // Apply gravity if (!this.grounded) { this.velocity.y -= 9.8 * dt; // Gravity } // s = s₀ + v·t const transform = this.entity.getComponent(TransformComponent); if (transform) { transform.position.addScaledVector(this.velocity, dt); } // Reset acceleration this.acceleration.set(0, 0, 0); // Basic collision with ground if (transform && transform.position.y < 0) { transform.position.y = 0; this.velocity.y = 0; this.grounded = true; } else { this.grounded = false; } } } // Health Component class HealthComponent extends Component { constructor(maxHealth = 100) { super(); this.maxHealth = maxHealth; this.currentHealth = maxHealth; this.armor = 0; } takeDamage(amount) { // DOOM-like armor damage reduction const armorAbsorption = 0.5; // Armor absorbs 50% of damage let armorDamage = 0; if (this.armor > 0) { armorDamage = Math.min(amount * armorAbsorption, this.armor); this.armor -= armorDamage; amount -= armorDamage; } this.currentHealth -= amount; if (this.currentHealth <= 0) { this.currentHealth = 0; this.entity.addTag('dead'); // Trigger death event if (this.entity.hasComponent(PlayerComponent)) { GameEvents.trigger('playerDeath'); } else { GameEvents.trigger('enemyDeath', this.entity); } } return amount; // Return actual damage dealt } heal(amount) { this.currentHealth = Math.min(this.currentHealth + amount, this.maxHealth); return this.currentHealth; } addArmor(amount) { this.armor += amount; return this.armor; } } // Player Component class PlayerComponent extends Component { constructor() { super(); this.moveSpeed = 5; this.rotationSpeed = 2; this.jumpForce = 5; // Player-specific properties this.currentWeaponIndex = 0; this.weapons = []; this.ammo = { bullets: 50, shells: 10, cells: 100, rockets: 5 }; // Doom-like stats this.kills = 0; this.secrets = 0; this.items = 0; } init() { // Add initial weapon this.addWeapon('pistol'); this.addWeapon('shotgun'); } addWeapon(weaponId) { // Check if we already have this weapon if (this.weapons.find(w => w.id === weaponId)) { return false; } const weapon = WEAPONS[weaponId]; if (weapon) { this.weapons.push({...weapon}); if (this.weapons.length === 1) { this.currentWeaponIndex = 0; } return true; } return false; } getCurrentWeapon() { if (this.weapons.length === 0) return null; return this.weapons[this.currentWeaponIndex]; } nextWeapon() { if (this.weapons.length === 0) return null; this.currentWeaponIndex = (this.currentWeaponIndex + 1) % this.weapons.length; GameEvents.trigger('weaponChanged', this.getCurrentWeapon()); return this.getCurrentWeapon(); } previousWeapon() { if (this.weapons.length === 0) return null; this.currentWeaponIndex = (this.currentWeaponIndex - 1 + this.weapons.length) % this.weapons.length; GameEvents.trigger('weaponChanged', this.getCurrentWeapon()); return this.getCurrentWeapon(); } hasAmmo(weaponId) { const weapon = WEAPONS[weaponId] || this.getCurrentWeapon(); return this.ammo[weapon.ammoType] >= weapon.ammoPerShot; } useAmmo(weapon) { if (!weapon) weapon = this.getCurrentWeapon(); if (this.ammo[weapon.ammoType] >= weapon.ammoPerShot) { this.ammo[weapon.ammoType] -= weapon.ammoPerShot; return true; } return false; } addAmmo(type, amount) { if (this.ammo[type] !== undefined) { const maxAmmo = { bullets: 200, shells: 50, rockets: 50, cells: 300 }; this.ammo[type] = Math.min(this.ammo[type] + amount, maxAmmo[type] || 100); return true; } return false; } } // Enemy Component class EnemyComponent extends Component { constructor(type) { super(); const enemyData = ENEMIES[type]; this.type = type; this.health = enemyData.health; this.damage = enemyData.damage; this.speed = enemyData.speed; this.attackRange = enemyData.attackRange; this.attackCooldown = enemyData.attackCooldown; this.lastAttack = 0; this.state = 'idle'; // idle, chase, attack, hurt, dead this.hasTarget = false; this.targetPosition = new THREE.Vector3(); this.attackSound = enemyData.attackSound; this.hurtSound = enemyData.hurtSound; this.deathSound = enemyData.deathSound; } update(dt) { // State machine for enemy behavior switch (this.state) { case 'idle': // Check for player visibility if (this.canSeePlayer()) { this.state = 'chase'; } break; case 'chase': // Move towards player this.chasePlayer(dt); // Check if in attack range if (this.isInAttackRange()) { this.state = 'attack'; } break; case 'attack': // Attack player this.attackPlayer(dt); // If player moved out of range, chase again if (!this.isInAttackRange()) { this.state = 'chase'; } break; case 'hurt': // Briefly stunned this.hurtTimer -= dt; if (this.hurtTimer <= 0) { this.state = 'chase'; } break; case 'dead': // Do nothing when dead break; } } canSeePlayer() { // Raycasting to check if player is visible // Simplified for this example const player = this.getPlayerEntity(); if (!player) return false; const transform = this.entity.getComponent(TransformComponent); const playerTransform = player.getComponent(TransformComponent); if (!transform || !playerTransform) return false; // Calculate distance to player const distance = transform.position.distanceTo(playerTransform.position); // Simple visibility check - can see player if within 20 units return distance < 20; } chasePlayer(dt) { const player = this.getPlayerEntity(); if (!player) return; const transform = this.entity.getComponent(TransformComponent); const playerTransform = player.getComponent(TransformComponent); if (!transform || !playerTransform) return; // Calculate direction to player const direction = new THREE.Vector3() .subVectors(playerTransform.position, transform.position) .normalize(); // Move towards player const physics = this.entity.getComponent(PhysicsComponent); if (physics) { physics.velocity.x = direction.x * this.speed; physics.velocity.z = direction.z * this.speed; // Rotate to face player transform.rotation.y = Math.atan2(direction.x, direction.z); } } isInAttackRange() { const player = this.getPlayerEntity(); if (!player) return false; const transform = this.entity.getComponent(TransformComponent); const playerTransform = player.getComponent(TransformComponent); if (!transform || !playerTransform) return false; // Calculate distance to player const distance = transform.position.distanceTo(playerTransform.position); // Check if within attack range return distance < this.attackRange; } attackPlayer(dt) { // Check cooldown const now = performance.now(); if (now - this.lastAttack < this.attackCooldown) return; const player = this.getPlayerEntity(); if (!player) return; // Deal damage to player const playerHealth = player.getComponent(HealthComponent); if (playerHealth) { playerHealth.takeDamage(this.damage); // Play attack sound if (this.attackSound) { AudioManager.playSound(this.attackSound); } } // Reset cooldown this.lastAttack = now; } takeDamage(amount) { this.health -= amount; // Play hurt sound if (this.hurtSound) { AudioManager.playSound(this.hurtSound); } if (this.health <= 0) { this.die(); } else { // Enter hurt state briefly this.state = 'hurt'; this.hurtTimer = 0.3; // 300ms stun } } die() { this.state = 'dead'; // Play death sound if (this.deathSound) { AudioManager.playSound(this.deathSound); } // Update player kill count const player = this.getPlayerEntity(); if (player) { const playerComponent = player.getComponent(PlayerComponent); if (playerComponent) { playerComponent.kills++; } } // Spawn death animation const transform = this.entity.getComponent(TransformComponent); if (transform) { // Create death animation at enemy position createDeathEffect(transform.position.clone()); } // Remove enemy after short delay (for death animation) setTimeout(() => { this.entity.destroy(); }, 1000); } getPlayerEntity() { // Find player entity return ECS.entities.find(entity => entity.hasTag('player')); } } // Weapon Component class WeaponComponent extends Component { constructor(weaponId) { super(); const weaponData = WEAPONS[weaponId]; this.id = weaponId; this.name = weaponData.name; this.damage = weaponData.damage; this.fireRate = weaponData.fireRate; this.range = weaponData.range; this.ammoType = weaponData.ammoType; this.ammoPerShot = weaponData.ammoPerShot; this.projectile = weaponData.projectile; this.spread = weaponData.spread || 0; this.bulletCount = weaponData.bulletCount || 1; this.lastFired = 0; this.mesh = null; this.muzzleFlash = null; this.fireSound = weaponData.fireSound; this.reloadSound = weaponData.reloadSound; } canFire() { const now = performance.now(); return now - this.lastFired > (1000 / this.fireRate); } fire(position, direction) { if (!this.canFire()) return false; // Check ammo const player = this.entity.getComponent(PlayerComponent); if (player && !player.useAmmo(this)) { // Play empty click sound AudioManager.playSound('weapons/empty'); return false; } // Update last fired time this.lastFired = performance.now(); // Play fire sound if (this.fireSound) { AudioManager.playSound(this.fireSound); } // Show muzzle flash this.showMuzzleFlash(); // Fire projectile or hitscan if (this.projectile) { // Spawn projectile entity this.spawnProjectile(position, direction); } else { // Perform hitscan shots for (let i = 0; i < this.bulletCount; i++) { this.fireHitscan(position, direction); } } return true; } fireHitscan(position, direction) { // Apply weapon spread const spreadVector = new THREE.Vector3( (Math.random() - 0.5) * this.spread, (Math.random() - 0.5) * this.spread, (Math.random() - 0.5) * this.spread ); const spreadDirection = direction.clone().add(spreadVector).normalize(); // Create raycaster for hitscan const raycaster = new THREE.Raycaster(position, spreadDirection, 0, this.range); // Get all potential targets (enemies and environment) const hitableEntities = ECS.entities.filter(entity => entity.hasComponent(ModelComponent) && !entity.hasTag('player') && !entity.hasTag('projectile') ); // Get meshes from entities const meshes = hitableEntities.map(entity => { const model = entity.getComponent(ModelComponent); return { entity, mesh: model.mesh }; }); // Check for intersections const intersects = raycaster.intersectObjects(meshes.map(m => m.mesh), true); if (intersects.length > 0) { const hit = intersects[0]; // Find which entity was hit const hitEntity = meshes.find(m => { const isParent = m.mesh === hit.object; const isChild = m.mesh.children.includes(hit.object); return isParent || isChild; })?.entity; if (hitEntity) { // If enemy was hit if (hitEntity.hasComponent(EnemyComponent)) { const enemy = hitEntity.getComponent(EnemyComponent); enemy.takeDamage(this.damage); // Create hit effect createBulletImpact(hit.point, hit.face.normal, 'blood'); } else { // Hit environment createBulletImpact(hit.point, hit.face.normal, 'wall'); } } } } spawnProjectile(position, direction) { // Create projectile entity const projectile = ECS.createEntity(); projectile.addTag('projectile'); // Add transform const transform = projectile.addComponent(TransformComponent, position.clone()); // Set rotation to match direction transform.rotation.y = Math.atan2(direction.x, direction.z); // Create projectile mesh const geometry = new THREE.SphereGeometry(0.2, 8, 8); const material = new THREE.MeshBasicMaterial({ color: 0xFF0000 }); const mesh = new THREE.Mesh(geometry, material); // Add light to projectile const light = new THREE.PointLight(0xFF0000, 1, 5); mesh.add(light); // Add model component projectile.addComponent(ModelComponent, mesh); // Add scene scene.add(mesh); // Add physics const physics = projectile.addComponent(PhysicsComponent, new THREE.Sphere(position, 0.2), 0.1); physics.velocity.copy(direction.clone().multiplyScalar(20)); // Projectile speed // Add projectile component projectile.addComponent(ProjectileComponent, this.damage, this.id); } showMuzzleFlash() { // Create muzzle flash if needed if (!this.muzzleFlash) { const geometry = new THREE.PlaneGeometry(1, 1); const material = new THREE.MeshBasicMaterial({ color: 0xFFFF00, transparent: true, opacity: 0.8, side: THREE.DoubleSide }); this.muzzleFlash = new THREE.Mesh(geometry, material); this.mesh.add(this.muzzleFlash); // Position muzzle flash at end of weapon this.muzzleFlash.position.z = -2; this.muzzleFlash.rotation.y = Math.PI / 2; // Add point light const light = new THREE.PointLight(0xFFFF00, 1, 5); this.muzzleFlash.add(light); } // Show muzzle flash this.muzzleFlash.visible = true; // Hide after a short delay setTimeout(() => { if (this.muzzleFlash) { this.muzzleFlash.visible = false; } }, 50); } } // Projectile Component class ProjectileComponent extends Component { constructor(damage, weaponId) { super(); this.damage = damage; this.weaponId = weaponId; this.lifetime = 5; // seconds before self-destruction } update(dt) { // Update lifetime this.lifetime -= dt; if (this.lifetime <= 0) { this.entity.destroy(); return; } // Check for collisions this.checkCollisions(); } checkCollisions() { const transform = this.entity.getComponent(TransformComponent); if (!transform) return; const position = transform.position; // Get all potential targets (enemies and environment) const hitableEntities = ECS.entities.filter(entity => entity.hasComponent(ModelComponent) && !entity.hasTag('projectile') && !entity.hasTag('player') // Don't hit player with own projectiles ); // Simple sphere collision check for (const targetEntity of hitableEntities) { const targetModel = targetEntity.getComponent(ModelComponent); const targetTransform = targetEntity.getComponent(TransformComponent); if (!targetModel || !targetTransform) continue; // Simple distance check const distance = position.distanceTo(targetTransform.position); // If within collision range if (distance < 1) { // Simple collision radius // Hit something! if (targetEntity.hasComponent(EnemyComponent)) { // Hit enemy const enemy = targetEntity.getComponent(EnemyComponent); enemy.takeDamage(this.damage); } // Create explosion effect this.explode(); // Destroy projectile this.entity.destroy(); return; } } } explode() { const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Create explosion entity const explosion = createExplosionEffect(transform.position); // Apply area damage const explosionRadius = 5; // Get all potential targets (enemies) const enemies = ECS.entities.filter(entity => entity.hasComponent(EnemyComponent)); // Apply damage to enemies in radius for (const enemy of enemies) { const enemyTransform = enemy.getComponent(TransformComponent); if (!enemyTransform) continue; // Calculate distance const distance = transform.position.distanceTo(enemyTransform.position); // If within explosion radius if (distance < explosionRadius) { // Calculate damage falloff based on distance const damageMultiplier = 1 - (distance / explosionRadius); const damage = this.damage * damageMultiplier; // Apply damage const enemyComponent = enemy.getComponent(EnemyComponent); enemyComponent.takeDamage(damage); } } } } // Item Component class ItemComponent extends Component { constructor(type, value) { super(); this.type = type; // health, armor, ammo, weapon this.subtype = null; // specific type (e.g. "bullets" for ammo) this.value = value; // amount to give this.pickupSound = null; // Set subtype and pickup sound based on type switch (type) { case 'health': this.pickupSound = 'items/health'; break; case 'armor': this.pickupSound = 'items/armor'; break; case 'ammo': this.subtype = value.type; this.value = value.amount; this.pickupSound = 'items/ammo'; break; case 'weapon': this.subtype = value; this.pickupSound = 'items/weapon'; break; } } update(dt) { // Make item rotate and bob const transform = this.entity.getComponent(TransformComponent); const model = this.entity.getComponent(ModelComponent); if (transform && model) { // Rotate slowly transform.rotation.y += dt; // Bob up and down const time = performance.now() / 1000; transform.position.y = 0.5 + Math.sin(time * 2) * 0.1; } // Check for player proximity this.checkPlayerPickup(); } checkPlayerPickup() { const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Find player const player = ECS.entities.find(entity => entity.hasTag('player')); if (!player) return; const playerTransform = player.getComponent(TransformComponent); if (!playerTransform) return; // Check distance const distance = transform.position.distanceTo(playerTransform.position); if (distance < 1.5) { // Pickup radius this.pickup(player); } } pickup(playerEntity) { const playerComponent = playerEntity.getComponent(PlayerComponent); const healthComponent = playerEntity.getComponent(HealthComponent); if (!playerComponent || !healthComponent) return false; let pickedUp = false; switch (this.type) { case 'health': // Only pickup if not at max health if (healthComponent.currentHealth < healthComponent.maxHealth) { healthComponent.heal(this.value); pickedUp = true; } break; case 'armor': // Always pickup armor healthComponent.addArmor(this.value); pickedUp = true; break; case 'ammo': // Add ammo pickedUp = playerComponent.addAmmo(this.subtype, this.value); break; case 'weapon': // Add weapon pickedUp = playerComponent.addWeapon(this.subtype); break; } if (pickedUp) { // Play pickup sound if (this.pickupSound) { AudioManager.playSound(this.pickupSound); } // Update items collected count playerComponent.items++; // Destroy item entity this.entity.destroy(); } return pickedUp; } } // Door Component class DoorComponent extends Component { constructor() { super(); this.isOpen = false; this.isMoving = false; this.openAmount = 0; // 0 = closed, 1 = open this.openSpeed = 2; // Units per second this.stayOpenTime = 3; // Seconds before auto-closing this.autoCloseTimer = 0; this.openSound = 'environment/door_open'; this.closeSound = 'environment/door_close'; } update(dt) { // Handle door movement if (this.isMoving) { if (this.isOpen) { // Opening this.openAmount += this.openSpeed * dt; if (this.openAmount >=// Opening this.openAmount += this.openSpeed * dt; if (this.openAmount >= 1) { this.openAmount = 1; this.isMoving = false; this.autoCloseTimer = this.stayOpenTime; } } else { // Closing this.openAmount -= this.openSpeed * dt; if (this.openAmount <= 0) { this.openAmount = 0; this.isMoving = false; } } // Update door position this.updateDoorPosition(); } // Handle auto-close timer if (this.isOpen && !this.isMoving) { this.autoCloseTimer -= dt; if (this.autoCloseTimer <= 0) { this.close(); } } // Check for player proximity this.checkPlayerInteraction(); } open() { if (this.isOpen || this.isMoving) return; this.isOpen = true; this.isMoving = true; // Play open sound if (this.openSound) { AudioManager.playSound(this.openSound); } } close() { if (!this.isOpen || this.isMoving) return; this.isOpen = false; this.isMoving = true; // Play close sound if (this.closeSound) { AudioManager.playSound(this.closeSound); } } checkPlayerInteraction() { const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Find player const player = ECS.entities.find(entity => entity.hasTag('player')); if (!player) return; const playerTransform = player.getComponent(TransformComponent); if (!playerTransform) return; // Check distance const distance = transform.position.distanceTo(playerTransform.position); if (distance < 2.5) { // Interaction radius // Open door when player is near this.open(); } } updateDoorPosition() { // Update door mesh based on open amount const model = this.entity.getComponent(ModelComponent); if (model && model.mesh) { // For sliding door, move along local x-axis model.mesh.position.x = this.openAmount * 2; // 2 units displacement when open } } } // Trigger Component class TriggerComponent extends Component { constructor(type, data) { super(); this.type = type; // Type of trigger: level, enemy, message, etc. this.data = data; // Additional data for trigger this.triggered = false; this.oneTime = true; // If true, only triggers once } update(dt) { if (this.triggered && this.oneTime) return; // Check for player intersection const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Find player const player = ECS.entities.find(entity => entity.hasTag('player')); if (!player) return; const playerTransform = player.getComponent(TransformComponent); if (!playerTransform) return; // Check distance const distance = transform.position.distanceTo(playerTransform.position); const triggerRadius = this.data.radius || 2; if (distance < triggerRadius) { this.activate(player); } } activate(playerEntity) { if (this.triggered && this.oneTime) return; this.triggered = true; switch (this.type) { case 'level': // Change level GameManager.loadLevel(this.data.levelId); break; case 'enemy': // Spawn enemies this.spawnEnemies(); break; case 'message': // Display message UI.showMessage(this.data.text, this.data.duration || 3); break; case 'secret': // Reveal secret this.revealSecret(playerEntity); break; case 'ambush': // Trigger enemy ambush this.triggerAmbush(); break; } } spawnEnemies() { const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Spawn enemies at specified positions if (Array.isArray(this.data.enemies)) { for (const enemyData of this.data.enemies) { const position = new THREE.Vector3( transform.position.x + enemyData.offset.x, transform.position.y + enemyData.offset.y, transform.position.z + enemyData.offset.z ); // Create enemy entity createEnemy(enemyData.type, position); } } } revealSecret(playerEntity) { // Play secret found sound AudioManager.playSound('game/secret'); // Update player stats const player = playerEntity.getComponent(PlayerComponent); if (player) { player.secrets++; } // Show message UI.showMessage("You found a secret area!", 3); // Reveal secret area - could open a hidden door, etc. if (this.data.doorEntity) { const door = this.data.doorEntity.getComponent(DoorComponent); if (door) { door.open(); } } } triggerAmbush() { // Play ambush sound AudioManager.playSound('game/ambush'); // Show message UI.showMessage("It's a trap!", 2); // Spawn enemies this.spawnEnemies(); // Could also lock doors, change lighting, etc. if (this.data.lockDoors) { // Find and lock doors const doors = ECS.entities.filter(entity => entity.hasComponent(DoorComponent)); for (const door of doors) { const doorComponent = door.getComponent(DoorComponent); doorComponent.close(); // Disable auto-opening temporarily doorComponent.checkPlayerInteraction = function() {}; // Restore normal behavior after delay setTimeout(() => { doorComponent.checkPlayerInteraction = DoorComponent.prototype.checkPlayerInteraction; }, 10000); // 10 seconds } } } } // Light Component class LightComponent extends Component { constructor(light) { super(); this.light = light; this.flickering = false; this.flickerIntensity = 0.2; this.flickerSpeed = 10; this.baseIntensity = light.intensity; } init() { const transform = this.entity.getComponent(TransformComponent); if (transform && this.light) { this.light.position.copy(transform.position); scene.add(this.light); } } update(dt) { // Update light position based on transform const transform = this.entity.getComponent(TransformComponent); if (transform && this.light) { this.light.position.copy(transform.position); } // Handle flickering effect if (this.flickering) { const time = performance.now() / 1000; const noise = Math.sin(time * this.flickerSpeed) * this.flickerIntensity; this.light.intensity = this.baseIntensity + noise; } } startFlickering(intensity, speed) { this.flickering = true; if (intensity !== undefined) this.flickerIntensity = intensity; if (speed !== undefined) this.flickerSpeed = speed; } stopFlickering() { this.flickering = false; this.light.intensity = this.baseIntensity; } } // Audio Component class AudioComponent extends Component { constructor(sound, options = {}) { super(); this.sound = sound; this.volume = options.volume || 1; this.loop = options.loop || false; this.autoplay = options.autoplay || false; this.maxDistance = options.maxDistance || 20; this.audioSource = null; } init() { this.audioSource = AudioManager.createPositionalSound( this.sound, this.volume, this.loop, this.maxDistance ); if (this.autoplay) { this.play(); } } update(dt) { // Update audio position based on transform const transform = this.entity.getComponent(TransformComponent); if (transform && this.audioSource) { this.audioSource.setPosition( transform.position.x, transform.position.y, transform.position.z ); } } play() { if (this.audioSource) { this.audioSource.play(); } } stop() { if (this.audioSource) { this.audioSource.stop(); } } setVolume(volume) { this.volume = volume; if (this.audioSource) { this.audioSource.setVolume(volume); } } } // Particle Emitter Component class ParticleEmitterComponent extends Component { constructor(options = {}) { super(); this.particleCount = options.count || 50; this.particleSize = options.size || 0.1; this.particleColor = options.color || 0xFFFFFF; this.particleLifetime = options.lifetime || 1; this.emissionRate = options.rate || 10; // particles per second this.emissionRadius = options.radius || 0.5; this.velocityRange = options.velocity || { min: 1, max: 3 }; this.gravity = options.gravity !== undefined ? options.gravity : true; this.particles = []; this.particleSystem = null; this.emitting = false; this.timeSinceLastEmission = 0; } init() { // Create particle geometry const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(this.particleCount * 3); const colors = new Float32Array(this.particleCount * 3); const sizes = new Float32Array(this.particleCount); // Initialize particle arrays for (let i = 0; i < this.particleCount; i++) { // Initialize offscreen positions[i * 3] = 0; positions[i * 3 + 1] = -100; // Hide below ground positions[i * 3 + 2] = 0; // Set colors const color = new THREE.Color(this.particleColor); colors[i * 3] = color.r; colors[i * 3 + 1] = color.g; colors[i * 3 + 2] = color.b; // Set sizes sizes[i] = this.particleSize; // Initialize particle data this.particles.push({ active: false, position: new THREE.Vector3(), velocity: new THREE.Vector3(), color: color.clone(), size: this.particleSize, lifetime: 0, maxLifetime: 0, index: i }); } // Set buffer attributes geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); // Create particle material const material = new THREE.PointsMaterial({ size: this.particleSize, vertexColors: true, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, sizeAttenuation: true }); // Create particle system this.particleSystem = new THREE.Points(geometry, material); scene.add(this.particleSystem); } update(dt) { if (!this.particleSystem) return; // Update emission if (this.emitting) { this.timeSinceLastEmission += dt; const emissionInterval = 1 / this.emissionRate; // Emit particles based on rate while (this.timeSinceLastEmission >= emissionInterval) { this.emitParticle(); this.timeSinceLastEmission -= emissionInterval; } } // Update existing particles const positions = this.particleSystem.geometry.attributes.position.array; const colors = this.particleSystem.geometry.attributes.color.array; const sizes = this.particleSystem.geometry.attributes.size.array; for (const particle of this.particles) { if (!particle.active) continue; // Update lifetime particle.lifetime -= dt; if (particle.lifetime <= 0) { // Deactivate particle particle.active = false; // Move off screen positions[particle.index * 3 + 1] = -100; } else { // Update position based on velocity particle.position.addScaledVector(particle.velocity, dt); // Apply gravity if (this.gravity) { particle.velocity.y -= 9.8 * dt; } // Update position in buffer positions[particle.index * 3] = particle.position.x; positions[particle.index * 3 + 1] = particle.position.y; positions[particle.index * 3 + 2] = particle.position.z; // Update opacity based on lifetime const lifeRatio = particle.lifetime / particle.maxLifetime; const color = particle.color.clone(); // Fade out colors[particle.index * 3] = color.r * lifeRatio; colors[particle.index * 3 + 1] = color.g * lifeRatio; colors[particle.index * 3 + 2] = color.b * lifeRatio; // Shrink sizes[particle.index] = particle.size * lifeRatio; } } // Mark buffers as needing update this.particleSystem.geometry.attributes.position.needsUpdate = true; this.particleSystem.geometry.attributes.color.needsUpdate = true; this.particleSystem.geometry.attributes.size.needsUpdate = true; } emitParticle() { // Find inactive particle const particle = this.particles.find(p => !p.active); if (!particle) return; // Get transform position const transform = this.entity.getComponent(TransformComponent); if (!transform) return; // Activate particle particle.active = true; // Set position particle.position.copy(transform.position); // Add random offset within emission radius const offset = new THREE.Vector3( (Math.random() - 0.5) * this.emissionRadius * 2, (Math.random() - 0.5) * this.emissionRadius * 2, (Math.random() - 0.5) * this.emissionRadius * 2 ); particle.position.add(offset); // Set velocity in random direction const speed = this.velocityRange.min + Math.random() * (this.velocityRange.max - this.velocityRange.min); const direction = new THREE.Vector3( Math.random() - 0.5, Math.random() - 0.5, Math.random() - 0.5 ).normalize(); particle.velocity.copy(direction).multiplyScalar(speed); // Set lifetime particle.maxLifetime = this.particleLifetime; particle.lifetime = this.particleLifetime; // Reset size particle.size = this.particleSize; // Update buffer positions const positions = this.particleSystem.geometry.attributes.position.array; positions[particle.index * 3] = particle.position.x; positions[particle.index * 3 + 1] = particle.position.y; positions[particle.index * 3 + 2] = particle.position.z; } startEmitting() { this.emitting = true; } stopEmitting() { this.emitting = false; } burstEmit(count) { const burstCount = count || this.particleCount / 4; for (let i = 0; i < burstCount; i++) { this.emitParticle(); } } } // ============================================================================ // Game Systems // ============================================================================ // Input System class InputSystem extends System { constructor() { super(); this.keys = {}; this.mouseButtons = {}; this.mousePosition = new THREE.Vector2(); this.mouseDelta = new THREE.Vector2(); this.pointerLocked = false; } init() { // Key events window.addEventListener('keydown', (e) => { this.keys[e.code] = true; }); window.addEventListener('keyup', (e) => { this.keys[e.code] = false; }); // Mouse events window.addEventListener('mousedown', (e) => { this.mouseButtons[e.button] = true; }); window.addEventListener('mouseup', (e) => { this.mouseButtons[e.button] = false; }); window.addEventListener('mousemove', (e) => { this.mousePosition.x = (e.clientX / window.innerWidth) * 2 - 1; this.mousePosition.y = -(e.clientY / window.innerHeight) * 2 + 1; if (this.pointerLocked) { this.mouseDelta.x = e.movementX; this.mouseDelta.y = e.movementY; } }); // Pointer lock document.addEventListener('click', () => { if (!this.pointerLocked) { document.body.requestPointerLock(); } }); document.addEventListener('pointerlockchange', () => { this.pointerLocked = document.pointerLockElement !== null; if (this.pointerLocked) { GameEvents.trigger('gameStarted'); } else { GameEvents.trigger('gamePaused'); } }); } update(dt) { // Reset mouse delta after each frame this.mouseDelta.set(0, 0); } isKeyDown(code) { return this.keys[code] === true; } isMouseButtonDown(button) { return this.mouseButtons[button] === true; } getMousePosition() { return this.mousePosition.clone(); } getMouseDelta() { return this.mouseDelta.clone(); } } // Player Controller System class PlayerControllerSystem extends System { constructor() { super(); this.mouseSensitivity = 0.002; this.lastWeaponScrollTime = 0; } update(dt) { const players = this.getEntitiesWith(PlayerComponent, TransformComponent); for (const player of players) { const playerComponent = player.getComponent(PlayerComponent); const transform = player.getComponent(TransformComponent); const physics = player.getComponent(PhysicsComponent); if (!playerComponent || !transform || !physics) continue; // Get input const inputSystem = ECS.systems.find(system => system instanceof InputSystem); if (!inputSystem) continue; // Handle mouse input for looking const mouseDelta = inputSystem.getMouseDelta(); transform.rotation.y -= mouseDelta.x * this.mouseSensitivity; // Movement input const moveSpeed = playerComponent.moveSpeed; let movementX = 0; let movementZ = 0; // WASD movement if (inputSystem.isKeyDown('KeyW')) { movementZ = -1; } if (inputSystem.isKeyDown('KeyS')) { movementZ = 1; } if (inputSystem.isKeyDown('KeyA')) { movementX = -1; } if (inputSystem.isKeyDown('KeyD')) { movementX = 1; } // Apply movement if (movementX !== 0 || movementZ !== 0) { // Create movement vector const moveVector = new THREE.Vector3(movementX, 0, movementZ).normalize(); // Create direction vector based on player rotation const forward = new THREE.Vector3(0, 0, -1).applyEuler(transform.rotation); const right = new THREE.Vector3(1, 0, 0).applyEuler(transform.rotation); // Calculate final movement direction const direction = new THREE.Vector3() .addScaledVector(forward, moveVector.z) .addScaledVector(right, moveVector.x) .normalize(); // Set velocity physics.velocity.x = direction.x * moveSpeed; physics.velocity.z = direction.z * moveSpeed; // Play footstep sounds this.handleFootsteps(player, physics); } else { // No movement, slow down physics.velocity.x *= 0.9; physics.velocity.z *= 0.9; } // Jump if (inputSystem.isKeyDown('Space') && physics.grounded) { physics.velocity.y = playerComponent.jumpForce; physics.grounded = false; // Play jump sound AudioManager.playSound('player/jump'); } // Weapon switching this.handleWeaponSwitching(player, inputSystem); // Weapon firing this.handleWeaponFiring(player, inputSystem); } } handleFootsteps(player, physics) { // Only play footsteps when grounded and moving if (!physics.grounded) return; // Calculate speed const speed = Math.sqrt(physics.velocity.x * physics.velocity.x + physics.velocity.z * physics.velocity.z); // Only play footsteps if moving fast enough if (speed < 1) return; // Play footstep sounds at rate based on speed const now = performance.now(); const stepTime = player.lastStepTime || 0; const stepInterval = 500 / speed; // Faster movement = faster steps if (now - stepTime > stepInterval) { // Play random footstep sound const footstepIndex = Math.floor(Math.random() * 4) + 1; AudioManager.playSound(`player/footstep${footstepIndex}`); // Update last step time player.lastStepTime = now; } } handleWeaponSwitching(player, inputSystem) { const playerComponent = player.getComponent(PlayerComponent); // Number keys for weapon selection for (let i = 1; i <= 9; i++) { if (inputSystem.isKeyDown(`Digit${i}`)) { const weaponIndex = i - 1; if (weaponIndex < playerComponent.weapons.length) { playerComponent.currentWeaponIndex = weaponIndex; GameEvents.trigger('weaponChanged', playerComponent.getCurrentWeapon()); // Play weapon switch sound AudioManager.playSound('weapons/switch'); break; } } } // Mouse wheel for weapon switching const now = performance.now(); if (now - this.lastWeaponScrollTime > 200) { // 200ms cooldown on weapon scrolling if (inputSystem.mouseScrollDelta > 0) { playerComponent.nextWeapon(); this.lastWeaponScrollTime = now; } else if (inputSystem.mouseScrollDelta < 0) { playerComponent.previousWeapon(); this.lastWeaponScrollTime = now; } } } handleWeaponFiring(player, inputSystem) { // Only handle firing if we have a weapon component const weaponComponent = player.getComponent(WeaponComponent); if (!weaponComponent) return; // Get player transform const transform = player.getComponent(TransformComponent); if (!transform) return; // Get current weapon const playerComponent = player.getComponent(PlayerComponent); const currentWeapon = playerComponent.getCurrentWeapon(); // Left click to fire if (inputSystem.isMouseButtonDown(0) && weaponComponent.canFire()) { // Get firing position and direction const position = transform.position.clone(); position.y += 1.7; // Eye height const direction = new THREE.Vector3(0, 0, -1).applyEuler(transform.rotation); // Fire weapon weaponComponent.fire(position, direction); } } } // Camera System class CameraSystem extends System { constructor() { super(); this.bobAmount = 0.05; this.bobSpeed = 10; this.swayAmount = 0.01; this.tiltAmount = 0.1; } update(dt) { const cameras = this.getEntitiesWith(CameraComponent); for (const entity of cameras) { const cameraComponent = entity.getComponent(CameraComponent); const transform = entity.getComponent(TransformComponent); const physics = entity.getComponent(PhysicsComponent); if (!cameraComponent || !transform) continue; // Update camera position cameraComponent.update(); // Apply head bob effect when moving if (physics && physics.grounded) { const speed = new THREE.Vector2(physics.velocity.x, physics.velocity.z).length(); if (speed > 0.5) { const bobTime = performance.now() / 1000 * this.bobSpeed; // Vertical bob const verticalBob = Math.sin(bobTime) * this.bobAmount * speed / 5; cameraComponent.camera.position.y += verticalBob; // Side-to-side bob const horizontalBob = Math.cos(bobTime / 2) * this.bobAmount * speed / 5; cameraComponent.camera.position.x += horizontalBob; // Slight rotation sway const rotationSway = Math.cos(bobTime) * this.swayAmount * speed / 5; cameraComponent.camera.rotation.z = rotationSway; } else { // Reset rotation when not moving cameraComponent.camera.rotation.z = 0; } } } } } // Enemy System class EnemySystem extends System { constructor() { super(); } update(dt) { const enemies = this.getEntitiesWith(EnemyComponent); for (const enemy of enemies) { const enemyComponent = enemy.getComponent(EnemyComponent); // Update enemy behavior enemyComponent.update(dt); } } } // Physics System class PhysicsSystem extends System { constructor() { super(); } update(dt) { const physicsEntities = this.getEntitiesWith(PhysicsComponent); // Update physics for each entity for (const entity of physicsEntities) { const physics = entity.getComponent(PhysicsComponent); physics.update(dt); } // Collision detection and resolution this.handleCollisions(physicsEntities); } handleCollisions(entities) { // Simple collision handling for (let i = 0; i < entities.length; i++) { const entityA = entities[i]; const physicsA = entityA.getComponent(PhysicsComponent); const transformA = entityA.getComponent(TransformComponent); if (!physicsA || !transformA) continue; for (let j = i + 1; j < entities.length; j++) { const entityB = entities[j]; const physicsB = entityB.getComponent(PhysicsComponent); const transformB = entityB.getComponent(TransformComponent); if (!physicsB || !transformB) continue; // Skip certain collision pairs if (entityA.hasTag('projectile') && entityB.hasTag('player')) continue; if (entityB.hasTag('projectile') && entityA.hasTag('player')) continue; // Simple sphere collision const distance = transformA.position.distanceTo(transformB.position); const minDistance = 1; // Simple collision distance if (distance < minDistance) { // Calculate collision response const direction = new THREE.Vector3() .subVectors(transformA.position, transformB.position) .normalize(); // Push entities apart const overlap = minDistance - distance; // If one entity is static (very high mass) if (physicsA.mass > 1000) { transformB.position.addScaledVector(direction.negate(), overlap); } else if (physicsB.mass > 1000) { transformA.position.addScaledVector(direction, overlap); } else { // Both dynamic - distribute displacement based on mass const totalMass = physicsA.mass + physicsB.mass; const ratioA = physicsB.mass / totalMass; const ratioB = physicsA.mass / totalMass; transformA.position.addScaledVector(direction, overlap * ratioA); transformB.position.addScaledVector(direction.negate(), overlap * ratioB); // Exchange momentum const v1 = physicsA.velocity.clone(); const v2 = physicsB.velocity.clone(); physicsA.velocity.addScaledVector(v2, 0.5); physicsB.velocity.addScaledVector(v1, 0.5); } } } } } }