All checks were successful
Build and Publish Docker Image / Build and Validate (pull_request) Successful in 9s
- 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.
309 lines
9.7 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
|