import { Transform } from '../components/Transform.js'; import { Velocity } from '../components/Velocity.js'; import { MeshComponent } from '../components/MeshComponent.js'; import { Collidable } from '../components/Collidable.js'; import { Health } from '../components/Health.js'; import { Invincibility } from '../components/Invincibility.js'; import { ObstacleType } from '../components/ObstacleType.js'; import { CoinType } from '../components/CoinType.js'; import { PowerUp } from '../components/PowerUp.js'; import { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js'; import { GameConfig } from './GameConfig.js'; /** * EntityFactory - creates pre-configured game entities with appropriate components. * Centralizes entity creation logic for consistency. * * @typedef {import('../ecs/World.js').EntityId} EntityId */ export class EntityFactory { /** * @param {import('../ecs/World.js').World} world - The ECS world * @param {THREE.Scene} scene - The Three.js scene */ constructor(world, scene) { /** @type {import('../ecs/World.js').World} */ this.world = world; /** @type {THREE.Scene} */ this.scene = scene; /** @type {number} Size of the game ground/play area */ this.groundSize = 30; } /** * Create the player entity * @returns {EntityId} The player entity ID */ createPlayer() { const entity = this.world.createEntity(); // Create mesh const geometry = new window.THREE.BoxGeometry(1, 1, 1); const material = new window.THREE.MeshStandardMaterial({ color: 0x4169E1, metalness: 0.3, roughness: 0.4 }); const mesh = new window.THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; this.scene.add(mesh); // Add components this.world.addComponent(entity, new Transform(0, 0.5, 0)); this.world.addComponent(entity, new Velocity()); this.world.addComponent(entity, new MeshComponent(mesh)); this.world.addComponent(entity, new Collidable(0, 'player')); // Player center point (original behavior) this.world.addComponent(entity, new Health(100)); // Invincibility starts inactive until first damage this.world.addComponent(entity, new Invincibility(GameConfig.INVINCIBILITY_DURATION, false)); this.world.addComponent(entity, new PlayerTag()); this.world.addComponent(entity, new BoundaryConstrained(this.groundSize)); return entity; } /** * Create a collectible coin entity * @param {number} [index=0] - Unique index for animation offset * @param {string} [type] - Optional coin type ('gold', 'silver', 'diamond', 'health') * @returns {EntityId} The coin entity ID */ createCoin(index = 0, type = null) { const entity = this.world.createEntity(); // Determine coin type (weighted random if not specified) let coinType = type; if (!coinType) { const rand = Math.random(); if (rand < 0.6) { coinType = 'gold'; // 60% gold } else if (rand < 0.85) { coinType = 'silver'; // 25% silver } else if (rand < 0.95) { coinType = 'diamond'; // 10% diamond } else { coinType = 'health'; // 5% health } } const typeComponent = new CoinType(coinType); // Create mesh with different colors/sizes based on type let size = 0.3; let color = 0xFFD700; // Gold let emissive = 0xFFD700; let emissiveIntensity = 0.3; if (coinType === 'silver') { color = 0xC0C0C0; emissive = 0xC0C0C0; size = 0.25; } else if (coinType === 'diamond') { color = 0x00FFFF; emissive = 0x00FFFF; emissiveIntensity = 0.6; size = 0.4; } else if (coinType === 'health') { color = 0x00FF00; emissive = 0x00FF00; emissiveIntensity = 0.4; size = 0.35; } const geometry = new window.THREE.SphereGeometry(size, 16, 16); const material = new window.THREE.MeshStandardMaterial({ color: color, metalness: 0.8, roughness: 0.2, emissive: emissive, emissiveIntensity: emissiveIntensity }); const mesh = new window.THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; this.scene.add(mesh); // Random position const x = (Math.random() - 0.5) * (this.groundSize - 4); const z = (Math.random() - 0.5) * (this.groundSize - 4); // Add components this.world.addComponent(entity, new Transform(x, 0.5, z)); this.world.addComponent(entity, new MeshComponent(mesh)); this.world.addComponent(entity, new Collidable(0.8, 'coin')); this.world.addComponent(entity, typeComponent); this.world.addComponent(entity, new CoinTag(index)); return entity; } /** * Create an obstacle entity * @param {string} [type] - Optional obstacle type ('normal', 'fast', 'chasing', 'spinning') * @returns {EntityId} The obstacle entity ID */ createObstacle(type = null) { const entity = this.world.createEntity(); // Determine obstacle type (weighted random if not specified) let obstacleType = type; if (!obstacleType) { const rand = Math.random(); if (rand < 0.5) { obstacleType = 'normal'; } else if (rand < 0.7) { obstacleType = 'fast'; } else if (rand < 0.85) { obstacleType = 'chasing'; } else { obstacleType = 'spinning'; } } const typeComponent = new ObstacleType(obstacleType); // Create mesh with different colors based on type const geometry = new window.THREE.BoxGeometry(1.5, 2, 1.5); let color = 0xFF4500; // Default orange-red if (obstacleType === 'fast') { color = 0xFF0000; // Red } else if (obstacleType === 'chasing') { color = 0x8B0000; // Dark red } else if (obstacleType === 'spinning') { color = 0xFF6347; // Tomato } const material = new window.THREE.MeshStandardMaterial({ color: color, metalness: 0.3, roughness: 0.7 }); const mesh = new window.THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; this.scene.add(mesh); // Random position (away from center) let posX, posZ; do { posX = (Math.random() - 0.5) * (this.groundSize - 4); posZ = (Math.random() - 0.5) * (this.groundSize - 4); } while (Math.abs(posX) < 3 && Math.abs(posZ) < 3); // Base velocity (will be modified by ObstacleSystem for different types) const baseSpeed = 0.05; const velocity = new Velocity( (Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier, 0, (Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier ); // Add components this.world.addComponent(entity, new Transform(posX, 1, posZ)); this.world.addComponent(entity, velocity); this.world.addComponent(entity, new MeshComponent(mesh)); this.world.addComponent(entity, new Collidable(1.5, 'obstacle')); this.world.addComponent(entity, typeComponent); this.world.addComponent(entity, new ObstacleTag()); this.world.addComponent(entity, new BoundaryConstrained(this.groundSize)); return entity; } /** * Create a power-up entity * @param {string} [type] - Optional power-up type ('speed', 'shield', 'multiplier', 'magnet') * @returns {EntityId} The power-up entity ID */ createPowerUp(type = null) { const entity = this.world.createEntity(); // Determine power-up type (random if not specified) let powerUpType = type; if (!powerUpType) { const rand = Math.random(); if (rand < 0.25) { powerUpType = 'speed'; } else if (rand < 0.5) { powerUpType = 'shield'; } else if (rand < 0.75) { powerUpType = 'multiplier'; } else { powerUpType = 'magnet'; } } // Get duration based on type let duration = 10; let color = 0x00FF00; let size = 0.4; switch (powerUpType) { case 'speed': duration = GameConfig.POWERUP_DURATION_SPEED; color = 0x00FFFF; // Cyan break; case 'shield': duration = GameConfig.POWERUP_DURATION_SHIELD; color = 0x0000FF; // Blue break; case 'multiplier': duration = GameConfig.POWERUP_DURATION_MULTIPLIER; color = 0xFF00FF; // Magenta break; case 'magnet': duration = GameConfig.POWERUP_DURATION_MAGNET; color = 0xFFFF00; // Yellow break; } const powerUpComponent = new PowerUp(powerUpType, duration); // Create mesh const geometry = new window.THREE.OctahedronGeometry(size, 0); const material = new window.THREE.MeshStandardMaterial({ color: color, metalness: 0.9, roughness: 0.1, emissive: color, emissiveIntensity: 0.5 }); const mesh = new window.THREE.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; this.scene.add(mesh); // Random position const x = (Math.random() - 0.5) * (this.groundSize - 4); const z = (Math.random() - 0.5) * (this.groundSize - 4); // Add components this.world.addComponent(entity, new Transform(x, 1, z)); this.world.addComponent(entity, new MeshComponent(mesh)); this.world.addComponent(entity, new Collidable(0.6, 'powerup')); this.world.addComponent(entity, powerUpComponent); return entity; } /** * Remove entity and its mesh from scene * @param {EntityId} entityId - The entity to destroy */ destroyEntity(entityId) { // Remove mesh from scene if it exists const meshComp = this.world.getComponent(entityId, MeshComponent); if (meshComp) { this.scene.remove(meshComp.mesh); meshComp.mesh.geometry.dispose(); meshComp.mesh.material.dispose(); } // Remove entity from world this.world.removeEntity(entityId); } }