threejs-test/src/game/EntityFactory.js
Juan Sebastian Montoya 4220e216e1
All checks were successful
Build and Publish Docker Image / Build and Validate (pull_request) Successful in 9s
feat: Introduce CoinType, ObstacleType, PowerUp components and systems
- Added CoinType component to define different coin types and their score values.
- Implemented ObstacleType component to manage various obstacle behaviors.
- Created PowerUp component to handle power-up types and durations.
- Integrated ParticleSystem for visual effects upon collecting coins and power-ups.
- Updated EntityFactory to create coins, obstacles, and power-ups with respective types.
- Enhanced Game class to manage power-up collection and effects, including score multipliers and health restoration.

This update enriches gameplay by adding collectible items with distinct behaviors and effects, enhancing player interaction and strategy.
2025-11-26 17:01:30 -05:00

309 lines
9.7 KiB
JavaScript

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);
}
}