feat: Introduce CoinType, ObstacleType, PowerUp components and systems
All checks were successful
Build and Publish Docker Image / Build and Validate (pull_request) Successful in 9s
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.
This commit is contained in:
parent
7ea49a1c9e
commit
4220e216e1
11 changed files with 885 additions and 33 deletions
39
src/components/CoinType.js
Normal file
39
src/components/CoinType.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* CoinType component - defines coin value and type
|
||||||
|
*/
|
||||||
|
export class CoinType {
|
||||||
|
/**
|
||||||
|
* @param {string} type - Type of coin: 'gold', 'silver', 'diamond', 'health'
|
||||||
|
*/
|
||||||
|
constructor(type = 'gold') {
|
||||||
|
/** @type {string} Coin type */
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
/** @type {number} Score value */
|
||||||
|
this.scoreValue = this.getScoreValue(type);
|
||||||
|
|
||||||
|
/** @type {number} Health restore amount (0 for non-health coins) */
|
||||||
|
this.healthRestore = type === 'health' ? 10 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get score value based on coin type
|
||||||
|
* @param {string} type - Coin type
|
||||||
|
* @returns {number} Score value
|
||||||
|
*/
|
||||||
|
getScoreValue(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'diamond':
|
||||||
|
return 50;
|
||||||
|
case 'gold':
|
||||||
|
return 10;
|
||||||
|
case 'silver':
|
||||||
|
return 5;
|
||||||
|
case 'health':
|
||||||
|
return 0; // Health coins don't give score
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
25
src/components/ObstacleType.js
Normal file
25
src/components/ObstacleType.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* ObstacleType component - defines obstacle behavior type
|
||||||
|
*/
|
||||||
|
export class ObstacleType {
|
||||||
|
/**
|
||||||
|
* @param {string} type - Type of obstacle: 'normal', 'fast', 'chasing', 'spinning'
|
||||||
|
*/
|
||||||
|
constructor(type = 'normal') {
|
||||||
|
/** @type {string} Obstacle type */
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
/** @type {number} Speed multiplier (1.0 = normal) */
|
||||||
|
this.speedMultiplier = type === 'fast' ? 1.5 : 1.0;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether this obstacle chases the player */
|
||||||
|
this.chases = type === 'chasing';
|
||||||
|
|
||||||
|
/** @type {boolean} Whether this obstacle spins */
|
||||||
|
this.spins = type === 'spinning';
|
||||||
|
|
||||||
|
/** @type {number} Rotation speed multiplier */
|
||||||
|
this.rotationSpeed = type === 'spinning' ? 3.0 : 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
28
src/components/ParticleEmitter.js
Normal file
28
src/components/ParticleEmitter.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* ParticleEmitter component - emits particles for visual effects
|
||||||
|
*/
|
||||||
|
export class ParticleEmitter {
|
||||||
|
/**
|
||||||
|
* @param {number} count - Number of particles to emit
|
||||||
|
* @param {number} lifetime - Lifetime of particles in seconds
|
||||||
|
* @param {number} color - Particle color (hex)
|
||||||
|
* @param {number} [speed=5] - Particle speed
|
||||||
|
*/
|
||||||
|
constructor(count, lifetime, color, speed = 5) {
|
||||||
|
/** @type {number} Number of particles */
|
||||||
|
this.count = count;
|
||||||
|
|
||||||
|
/** @type {number} Lifetime in seconds */
|
||||||
|
this.lifetime = lifetime;
|
||||||
|
|
||||||
|
/** @type {number} Particle color */
|
||||||
|
this.color = color;
|
||||||
|
|
||||||
|
/** @type {number} Particle speed */
|
||||||
|
this.speed = speed;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether emitter is active */
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
42
src/components/PowerUp.js
Normal file
42
src/components/PowerUp.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* PowerUp component - defines power-up type and duration
|
||||||
|
*/
|
||||||
|
export class PowerUp {
|
||||||
|
/**
|
||||||
|
* @param {string} type - Type of power-up: 'speed', 'shield', 'multiplier', 'magnet'
|
||||||
|
* @param {number} [duration=10] - Duration in seconds
|
||||||
|
*/
|
||||||
|
constructor(type, duration = 10) {
|
||||||
|
/** @type {string} Power-up type */
|
||||||
|
this.type = type;
|
||||||
|
|
||||||
|
/** @type {number} Duration in seconds */
|
||||||
|
this.duration = duration;
|
||||||
|
|
||||||
|
/** @type {number} Time remaining */
|
||||||
|
this.timeRemaining = duration;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether power-up is active */
|
||||||
|
this.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update power-up timer
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
* @returns {boolean} True if still active
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
if (!this.active) return false;
|
||||||
|
|
||||||
|
this.timeRemaining -= deltaTime;
|
||||||
|
|
||||||
|
if (this.timeRemaining <= 0) {
|
||||||
|
this.active = false;
|
||||||
|
this.timeRemaining = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { MeshComponent } from '../components/MeshComponent.js';
|
||||||
import { Collidable } from '../components/Collidable.js';
|
import { Collidable } from '../components/Collidable.js';
|
||||||
import { Health } from '../components/Health.js';
|
import { Health } from '../components/Health.js';
|
||||||
import { Invincibility } from '../components/Invincibility.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 { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
||||||
import { GameConfig } from './GameConfig.js';
|
import { GameConfig } from './GameConfig.js';
|
||||||
|
|
||||||
|
|
@ -65,19 +68,58 @@ export class EntityFactory {
|
||||||
/**
|
/**
|
||||||
* Create a collectible coin entity
|
* Create a collectible coin entity
|
||||||
* @param {number} [index=0] - Unique index for animation offset
|
* @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
|
* @returns {EntityId} The coin entity ID
|
||||||
*/
|
*/
|
||||||
createCoin(index = 0) {
|
createCoin(index = 0, type = null) {
|
||||||
const entity = this.world.createEntity();
|
const entity = this.world.createEntity();
|
||||||
|
|
||||||
// Create mesh
|
// Determine coin type (weighted random if not specified)
|
||||||
const geometry = new window.THREE.SphereGeometry(0.3, 16, 16);
|
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({
|
const material = new window.THREE.MeshStandardMaterial({
|
||||||
color: 0xFFD700,
|
color: color,
|
||||||
metalness: 0.8,
|
metalness: 0.8,
|
||||||
roughness: 0.2,
|
roughness: 0.2,
|
||||||
emissive: 0xFFD700,
|
emissive: emissive,
|
||||||
emissiveIntensity: 0.3
|
emissiveIntensity: emissiveIntensity
|
||||||
});
|
});
|
||||||
const mesh = new window.THREE.Mesh(geometry, material);
|
const mesh = new window.THREE.Mesh(geometry, material);
|
||||||
mesh.castShadow = true;
|
mesh.castShadow = true;
|
||||||
|
|
@ -92,6 +134,7 @@ export class EntityFactory {
|
||||||
this.world.addComponent(entity, new Transform(x, 0.5, z));
|
this.world.addComponent(entity, new Transform(x, 0.5, z));
|
||||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||||
this.world.addComponent(entity, new Collidable(0.8, 'coin'));
|
this.world.addComponent(entity, new Collidable(0.8, 'coin'));
|
||||||
|
this.world.addComponent(entity, typeComponent);
|
||||||
this.world.addComponent(entity, new CoinTag(index));
|
this.world.addComponent(entity, new CoinTag(index));
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
|
|
@ -99,15 +142,42 @@ export class EntityFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an obstacle entity
|
* Create an obstacle entity
|
||||||
|
* @param {string} [type] - Optional obstacle type ('normal', 'fast', 'chasing', 'spinning')
|
||||||
* @returns {EntityId} The obstacle entity ID
|
* @returns {EntityId} The obstacle entity ID
|
||||||
*/
|
*/
|
||||||
createObstacle() {
|
createObstacle(type = null) {
|
||||||
const entity = this.world.createEntity();
|
const entity = this.world.createEntity();
|
||||||
|
|
||||||
// Create mesh
|
// 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);
|
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({
|
const material = new window.THREE.MeshStandardMaterial({
|
||||||
color: 0xFF4500,
|
color: color,
|
||||||
metalness: 0.3,
|
metalness: 0.3,
|
||||||
roughness: 0.7
|
roughness: 0.7
|
||||||
});
|
});
|
||||||
|
|
@ -123,11 +193,12 @@ export class EntityFactory {
|
||||||
posZ = (Math.random() - 0.5) * (this.groundSize - 4);
|
posZ = (Math.random() - 0.5) * (this.groundSize - 4);
|
||||||
} while (Math.abs(posX) < 3 && Math.abs(posZ) < 3);
|
} while (Math.abs(posX) < 3 && Math.abs(posZ) < 3);
|
||||||
|
|
||||||
// Random velocity
|
// Base velocity (will be modified by ObstacleSystem for different types)
|
||||||
|
const baseSpeed = 0.05;
|
||||||
const velocity = new Velocity(
|
const velocity = new Velocity(
|
||||||
(Math.random() - 0.5) * 0.05,
|
(Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier,
|
||||||
0,
|
0,
|
||||||
(Math.random() - 0.5) * 0.05
|
(Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add components
|
// Add components
|
||||||
|
|
@ -135,12 +206,89 @@ export class EntityFactory {
|
||||||
this.world.addComponent(entity, velocity);
|
this.world.addComponent(entity, velocity);
|
||||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||||
this.world.addComponent(entity, new Collidable(1.5, 'obstacle'));
|
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 ObstacleTag());
|
||||||
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
||||||
|
|
||||||
return entity;
|
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
|
* Remove entity and its mesh from scene
|
||||||
* @param {EntityId} entityId - The entity to destroy
|
* @param {EntityId} entityId - The entity to destroy
|
||||||
|
|
|
||||||
265
src/game/Game.js
265
src/game/Game.js
|
|
@ -11,12 +11,16 @@ import { CoinSystem } from '../systems/CoinSystem.js';
|
||||||
import { ObstacleSystem } from '../systems/ObstacleSystem.js';
|
import { ObstacleSystem } from '../systems/ObstacleSystem.js';
|
||||||
import { CollisionSystem } from '../systems/CollisionSystem.js';
|
import { CollisionSystem } from '../systems/CollisionSystem.js';
|
||||||
import { InvincibilitySystem } from '../systems/InvincibilitySystem.js';
|
import { InvincibilitySystem } from '../systems/InvincibilitySystem.js';
|
||||||
|
import { ParticleSystem } from '../systems/ParticleSystem.js';
|
||||||
|
import { PowerUpSystem } from '../systems/PowerUpSystem.js';
|
||||||
import { RenderSystem } from '../systems/RenderSystem.js';
|
import { RenderSystem } from '../systems/RenderSystem.js';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { Transform } from '../components/Transform.js';
|
import { Transform } from '../components/Transform.js';
|
||||||
import { Health } from '../components/Health.js';
|
import { Health } from '../components/Health.js';
|
||||||
import { Invincibility } from '../components/Invincibility.js';
|
import { Invincibility } from '../components/Invincibility.js';
|
||||||
|
import { CoinType } from '../components/CoinType.js';
|
||||||
|
import { PowerUp } from '../components/PowerUp.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Game class - manages the game loop and coordinates all systems.
|
* Main Game class - manages the game loop and coordinates all systems.
|
||||||
|
|
@ -46,6 +50,9 @@ export class Game {
|
||||||
|
|
||||||
/** @type {EntityId[]} Array of obstacle entity IDs */
|
/** @type {EntityId[]} Array of obstacle entity IDs */
|
||||||
this.obstacles = [];
|
this.obstacles = [];
|
||||||
|
|
||||||
|
/** @type {EntityId[]} Array of power-up entity IDs */
|
||||||
|
this.powerUps = [];
|
||||||
|
|
||||||
/** @type {number} Last frame timestamp for deltaTime calculation */
|
/** @type {number} Last frame timestamp for deltaTime calculation */
|
||||||
this.lastTime = performance.now();
|
this.lastTime = performance.now();
|
||||||
|
|
@ -89,6 +96,23 @@ export class Game {
|
||||||
/** @type {number} Time since last health regeneration */
|
/** @type {number} Time since last health regeneration */
|
||||||
this.healthRegenTimer = 0;
|
this.healthRegenTimer = 0;
|
||||||
|
|
||||||
|
// Screen shake state
|
||||||
|
/** @type {number} Remaining screen shake time */
|
||||||
|
this.screenShakeTime = 0;
|
||||||
|
|
||||||
|
/** @type {import('three').Vector3} Original camera position offset */
|
||||||
|
this.cameraBaseOffset = new window.THREE.Vector3(0, 10, 15);
|
||||||
|
|
||||||
|
// Difficulty scaling state
|
||||||
|
/** @type {number} Game start time */
|
||||||
|
this.gameStartTime = performance.now() / 1000;
|
||||||
|
|
||||||
|
/** @type {number} Last score when obstacle was added */
|
||||||
|
this.lastDifficultyScore = 0;
|
||||||
|
|
||||||
|
/** @type {number} Last time when obstacle was added */
|
||||||
|
this.lastDifficultyTime = 0;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.animate();
|
this.animate();
|
||||||
|
|
@ -186,13 +210,17 @@ export class Game {
|
||||||
this.inputSystem = new InputSystem();
|
this.inputSystem = new InputSystem();
|
||||||
this.world.addSystem(this.inputSystem);
|
this.world.addSystem(this.inputSystem);
|
||||||
|
|
||||||
// Player control
|
// Player control (will set power-up system after it's created)
|
||||||
this.world.addSystem(new PlayerControlSystem(this.inputSystem));
|
this.playerControlSystem = new PlayerControlSystem(this.inputSystem);
|
||||||
|
this.world.addSystem(this.playerControlSystem);
|
||||||
|
|
||||||
// Movement and physics
|
// Movement and physics
|
||||||
this.world.addSystem(new MovementSystem());
|
this.world.addSystem(new MovementSystem());
|
||||||
this.world.addSystem(new BoundarySystem());
|
this.world.addSystem(new BoundarySystem());
|
||||||
this.world.addSystem(new ObstacleSystem());
|
|
||||||
|
// Obstacle system (will set player entity after player is created)
|
||||||
|
this.obstacleSystem = new ObstacleSystem();
|
||||||
|
this.world.addSystem(this.obstacleSystem);
|
||||||
|
|
||||||
// Game-specific behavior
|
// Game-specific behavior
|
||||||
this.world.addSystem(new CoinSystem());
|
this.world.addSystem(new CoinSystem());
|
||||||
|
|
@ -200,6 +228,19 @@ export class Game {
|
||||||
// Invincibility system (before collision to update state)
|
// Invincibility system (before collision to update state)
|
||||||
this.world.addSystem(new InvincibilitySystem());
|
this.world.addSystem(new InvincibilitySystem());
|
||||||
|
|
||||||
|
// Particle system
|
||||||
|
this.particleSystem = new ParticleSystem(this.scene);
|
||||||
|
this.world.addSystem(this.particleSystem);
|
||||||
|
|
||||||
|
// Power-up system (will set player entity after player is created)
|
||||||
|
this.powerUpSystem = new PowerUpSystem();
|
||||||
|
this.world.addSystem(this.powerUpSystem);
|
||||||
|
|
||||||
|
// Connect power-up system to player control system
|
||||||
|
if (this.playerControlSystem) {
|
||||||
|
this.playerControlSystem.setPowerUpSystem(this.powerUpSystem);
|
||||||
|
}
|
||||||
|
|
||||||
// Collision detection
|
// Collision detection
|
||||||
this.collisionSystem = new CollisionSystem();
|
this.collisionSystem = new CollisionSystem();
|
||||||
this.collisionSystem.onCollision((entity1, entity2, layer1, layer2) => {
|
this.collisionSystem.onCollision((entity1, entity2, layer1, layer2) => {
|
||||||
|
|
@ -214,6 +255,14 @@ export class Game {
|
||||||
createGameEntities() {
|
createGameEntities() {
|
||||||
// Create player
|
// Create player
|
||||||
this.playerEntity = this.entityFactory.createPlayer();
|
this.playerEntity = this.entityFactory.createPlayer();
|
||||||
|
|
||||||
|
// Set player entity in systems that need it
|
||||||
|
if (this.obstacleSystem) {
|
||||||
|
this.obstacleSystem.setPlayerEntity(this.playerEntity);
|
||||||
|
}
|
||||||
|
if (this.powerUpSystem) {
|
||||||
|
this.powerUpSystem.setPlayerEntity(this.playerEntity);
|
||||||
|
}
|
||||||
|
|
||||||
// Create coins
|
// Create coins
|
||||||
for (let i = 0; i < GameConfig.INITIAL_COIN_COUNT; i++) {
|
for (let i = 0; i < GameConfig.INITIAL_COIN_COUNT; i++) {
|
||||||
|
|
@ -221,7 +270,7 @@ export class Game {
|
||||||
this.coins.push(coin);
|
this.coins.push(coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create obstacles
|
// Create obstacles (mix of types)
|
||||||
for (let i = 0; i < GameConfig.INITIAL_OBSTACLE_COUNT; i++) {
|
for (let i = 0; i < GameConfig.INITIAL_OBSTACLE_COUNT; i++) {
|
||||||
const obstacle = this.entityFactory.createObstacle();
|
const obstacle = this.entityFactory.createObstacle();
|
||||||
this.obstacles.push(obstacle);
|
this.obstacles.push(obstacle);
|
||||||
|
|
@ -237,6 +286,12 @@ export class Game {
|
||||||
this.collectCoin(coinEntity);
|
this.collectCoin(coinEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Player-PowerUp collision
|
||||||
|
if ((layer1 === 'player' && layer2 === 'powerup') || (layer1 === 'powerup' && layer2 === 'player')) {
|
||||||
|
const powerUpEntity = layer1 === 'powerup' ? entity1 : entity2;
|
||||||
|
this.collectPowerUp(powerUpEntity);
|
||||||
|
}
|
||||||
|
|
||||||
// Player-Obstacle collision
|
// Player-Obstacle collision
|
||||||
if ((layer1 === 'player' && layer2 === 'obstacle') || (layer1 === 'obstacle' && layer2 === 'player')) {
|
if ((layer1 === 'player' && layer2 === 'obstacle') || (layer1 === 'obstacle' && layer2 === 'player')) {
|
||||||
const playerEntity = layer1 === 'player' ? entity1 : entity2;
|
const playerEntity = layer1 === 'player' ? entity1 : entity2;
|
||||||
|
|
@ -246,6 +301,11 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectCoin(coinEntity) {
|
collectCoin(coinEntity) {
|
||||||
|
// Get coin position and type before destroying
|
||||||
|
const coinTransform = this.world.getComponent(coinEntity, Transform);
|
||||||
|
const coinType = this.world.getComponent(coinEntity, CoinType);
|
||||||
|
const coinPosition = coinTransform ? coinTransform.position.clone() : null;
|
||||||
|
|
||||||
// Remove coin
|
// Remove coin
|
||||||
this.entityFactory.destroyEntity(coinEntity);
|
this.entityFactory.destroyEntity(coinEntity);
|
||||||
const index = this.coins.indexOf(coinEntity);
|
const index = this.coins.indexOf(coinEntity);
|
||||||
|
|
@ -253,7 +313,42 @@ export class Game {
|
||||||
this.coins.splice(index, 1);
|
this.coins.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update combo system
|
// Determine particle color based on coin type
|
||||||
|
let particleColor = 0xFFD700; // Default gold
|
||||||
|
if (coinType) {
|
||||||
|
if (coinType.type === 'silver') {
|
||||||
|
particleColor = 0xC0C0C0;
|
||||||
|
} else if (coinType.type === 'diamond') {
|
||||||
|
particleColor = 0x00FFFF;
|
||||||
|
} else if (coinType.type === 'health') {
|
||||||
|
particleColor = 0x00FF00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit particles for coin collection
|
||||||
|
if (coinPosition && this.particleSystem) {
|
||||||
|
this.particleSystem.emit(
|
||||||
|
coinPosition,
|
||||||
|
GameConfig.PARTICLE_COUNT_COIN,
|
||||||
|
particleColor,
|
||||||
|
8
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle health coin
|
||||||
|
if (coinType && coinType.type === 'health') {
|
||||||
|
const health = this.world.getComponent(this.playerEntity, Health);
|
||||||
|
if (health) {
|
||||||
|
health.heal(coinType.healthRestore);
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
// Health coins don't contribute to combo or score
|
||||||
|
const newCoin = this.entityFactory.createCoin(this.coins.length);
|
||||||
|
this.coins.push(newCoin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update combo system (only for score coins)
|
||||||
const currentTime = performance.now() / 1000; // Convert to seconds
|
const currentTime = performance.now() / 1000; // Convert to seconds
|
||||||
const timeSinceLastCoin = currentTime - this.lastCoinTime;
|
const timeSinceLastCoin = currentTime - this.lastCoinTime;
|
||||||
|
|
||||||
|
|
@ -272,9 +367,10 @@ export class Game {
|
||||||
|
|
||||||
this.lastCoinTime = currentTime;
|
this.lastCoinTime = currentTime;
|
||||||
|
|
||||||
// Calculate score with combo multiplier
|
// Calculate score with combo multiplier and power-up multiplier (use coin's base value)
|
||||||
const baseScore = GameConfig.COMBO_BASE_SCORE;
|
const baseScore = coinType ? coinType.scoreValue : GameConfig.COMBO_BASE_SCORE;
|
||||||
const scoreGain = baseScore * this.comboMultiplier;
|
const powerUpMultiplier = this.powerUpSystem ? this.powerUpSystem.scoreMultiplier : 1.0;
|
||||||
|
const scoreGain = baseScore * this.comboMultiplier * powerUpMultiplier;
|
||||||
this.score += scoreGain;
|
this.score += scoreGain;
|
||||||
|
|
||||||
// Check for new high score
|
// Check for new high score
|
||||||
|
|
@ -285,12 +381,85 @@ export class Game {
|
||||||
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Spawn new coin
|
// Spawn new coin or power-up (based on chance)
|
||||||
const newCoin = this.entityFactory.createCoin(this.coins.length);
|
if (Math.random() < GameConfig.POWERUP_SPAWN_CHANCE) {
|
||||||
this.coins.push(newCoin);
|
const powerUp = this.entityFactory.createPowerUp();
|
||||||
|
this.powerUps.push(powerUp);
|
||||||
|
} else {
|
||||||
|
const newCoin = this.entityFactory.createCoin(this.coins.length);
|
||||||
|
this.coins.push(newCoin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectPowerUp(powerUpEntity) {
|
||||||
|
// Get power-up position and type before destroying
|
||||||
|
const powerUpTransform = this.world.getComponent(powerUpEntity, Transform);
|
||||||
|
const powerUp = this.world.getComponent(powerUpEntity, PowerUp);
|
||||||
|
const powerUpPosition = powerUpTransform ? powerUpTransform.position.clone() : null;
|
||||||
|
|
||||||
|
// Remove power-up
|
||||||
|
this.entityFactory.destroyEntity(powerUpEntity);
|
||||||
|
const index = this.powerUps.indexOf(powerUpEntity);
|
||||||
|
if (index > -1) {
|
||||||
|
this.powerUps.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate power-up effect
|
||||||
|
if (powerUp && this.powerUpSystem) {
|
||||||
|
this.powerUpSystem.activatePowerUp(powerUp.type, powerUp.duration);
|
||||||
|
|
||||||
|
// Special handling for shield - activate invincibility
|
||||||
|
if (powerUp.type === 'shield') {
|
||||||
|
const invincibility = this.world.getComponent(this.playerEntity, Invincibility);
|
||||||
|
if (invincibility) {
|
||||||
|
invincibility.activate(powerUp.duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit particles
|
||||||
|
if (powerUpPosition && this.particleSystem) {
|
||||||
|
let particleColor = 0x00FF00;
|
||||||
|
if (powerUp) {
|
||||||
|
switch (powerUp.type) {
|
||||||
|
case 'speed':
|
||||||
|
particleColor = 0x00FFFF;
|
||||||
|
break;
|
||||||
|
case 'shield':
|
||||||
|
particleColor = 0x0000FF;
|
||||||
|
break;
|
||||||
|
case 'multiplier':
|
||||||
|
particleColor = 0xFF00FF;
|
||||||
|
break;
|
||||||
|
case 'magnet':
|
||||||
|
particleColor = 0xFFFF00;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.particleSystem.emit(
|
||||||
|
powerUpPosition,
|
||||||
|
GameConfig.PARTICLE_COUNT_COIN,
|
||||||
|
particleColor,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn new coin or power-up
|
||||||
|
if (Math.random() < GameConfig.POWERUP_SPAWN_CHANCE) {
|
||||||
|
const newPowerUp = this.entityFactory.createPowerUp();
|
||||||
|
this.powerUps.push(newPowerUp);
|
||||||
|
} else {
|
||||||
|
const newCoin = this.entityFactory.createCoin(this.coins.length);
|
||||||
|
this.coins.push(newCoin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleObstacleCollision(playerEntity, obstacleEntity) {
|
handleObstacleCollision(playerEntity, obstacleEntity) {
|
||||||
|
// Check if player has shield power-up active
|
||||||
|
if (this.powerUpSystem && this.powerUpSystem.isActive('shield')) {
|
||||||
|
return; // No damage if shield is active
|
||||||
|
}
|
||||||
|
|
||||||
// Check if player is invincible
|
// Check if player is invincible
|
||||||
const invincibility = this.world.getComponent(playerEntity, Invincibility);
|
const invincibility = this.world.getComponent(playerEntity, Invincibility);
|
||||||
if (invincibility && invincibility.getIsInvincible()) {
|
if (invincibility && invincibility.getIsInvincible()) {
|
||||||
|
|
@ -309,6 +478,19 @@ export class Game {
|
||||||
invincibility.activate(GameConfig.INVINCIBILITY_DURATION);
|
invincibility.activate(GameConfig.INVINCIBILITY_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Screen shake on damage
|
||||||
|
this.screenShakeTime = GameConfig.SCREEN_SHAKE_DURATION;
|
||||||
|
|
||||||
|
// Emit damage particles
|
||||||
|
if (this.particleSystem && playerTransform) {
|
||||||
|
this.particleSystem.emit(
|
||||||
|
playerTransform.position.clone().add(new window.THREE.Vector3(0, 0.5, 0)),
|
||||||
|
GameConfig.PARTICLE_COUNT_DAMAGE,
|
||||||
|
0xFF0000, // Red color
|
||||||
|
6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Push player back
|
// Push player back
|
||||||
const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position);
|
const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position);
|
||||||
pushDirection.y = 0;
|
pushDirection.y = 0;
|
||||||
|
|
@ -332,8 +514,20 @@ export class Game {
|
||||||
|
|
||||||
const playerTransform = this.world.getComponent(this.playerEntity, Transform);
|
const playerTransform = this.world.getComponent(this.playerEntity, Transform);
|
||||||
if (playerTransform) {
|
if (playerTransform) {
|
||||||
this.camera.position.x = playerTransform.position.x;
|
// Base camera position
|
||||||
this.camera.position.z = playerTransform.position.z + 15;
|
let cameraX = playerTransform.position.x;
|
||||||
|
let cameraZ = playerTransform.position.z + 15;
|
||||||
|
let cameraY = 10;
|
||||||
|
|
||||||
|
// Apply screen shake
|
||||||
|
if (this.screenShakeTime > 0) {
|
||||||
|
const intensity = (this.screenShakeTime / GameConfig.SCREEN_SHAKE_DURATION) * GameConfig.SCREEN_SHAKE_INTENSITY;
|
||||||
|
cameraX += (Math.random() - 0.5) * intensity;
|
||||||
|
cameraY += (Math.random() - 0.5) * intensity;
|
||||||
|
cameraZ += (Math.random() - 0.5) * intensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.camera.position.set(cameraX, cameraY, cameraZ);
|
||||||
this.camera.lookAt(playerTransform.position);
|
this.camera.lookAt(playerTransform.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -383,12 +577,14 @@ export class Game {
|
||||||
// Clean up old entities
|
// Clean up old entities
|
||||||
[...this.coins].forEach(coin => this.entityFactory.destroyEntity(coin));
|
[...this.coins].forEach(coin => this.entityFactory.destroyEntity(coin));
|
||||||
[...this.obstacles].forEach(obstacle => this.entityFactory.destroyEntity(obstacle));
|
[...this.obstacles].forEach(obstacle => this.entityFactory.destroyEntity(obstacle));
|
||||||
|
[...this.powerUps].forEach(powerUp => this.entityFactory.destroyEntity(powerUp));
|
||||||
if (this.playerEntity) {
|
if (this.playerEntity) {
|
||||||
this.entityFactory.destroyEntity(this.playerEntity);
|
this.entityFactory.destroyEntity(this.playerEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.coins = [];
|
this.coins = [];
|
||||||
this.obstacles = [];
|
this.obstacles = [];
|
||||||
|
this.powerUps = [];
|
||||||
|
|
||||||
// Reset game state
|
// Reset game state
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
|
|
@ -402,6 +598,14 @@ export class Game {
|
||||||
|
|
||||||
// Reset health regeneration timer
|
// Reset health regeneration timer
|
||||||
this.healthRegenTimer = 0;
|
this.healthRegenTimer = 0;
|
||||||
|
|
||||||
|
// Reset screen shake
|
||||||
|
this.screenShakeTime = 0;
|
||||||
|
|
||||||
|
// Reset difficulty scaling
|
||||||
|
this.gameStartTime = performance.now() / 1000;
|
||||||
|
this.lastDifficultyScore = 0;
|
||||||
|
this.lastDifficultyTime = 0;
|
||||||
|
|
||||||
// Recreate entities
|
// Recreate entities
|
||||||
this.createGameEntities();
|
this.createGameEntities();
|
||||||
|
|
@ -531,6 +735,33 @@ export class Game {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update difficulty scaling - spawn more obstacles over time
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
*/
|
||||||
|
updateDifficulty(deltaTime) {
|
||||||
|
const currentTime = performance.now() / 1000;
|
||||||
|
const elapsedTime = currentTime - this.gameStartTime;
|
||||||
|
|
||||||
|
// Check if we should add an obstacle based on score
|
||||||
|
const scoreDiff = this.score - this.lastDifficultyScore;
|
||||||
|
if (scoreDiff >= GameConfig.DIFFICULTY_SCORE_INTERVAL &&
|
||||||
|
this.obstacles.length < GameConfig.MAX_OBSTACLES) {
|
||||||
|
const newObstacle = this.entityFactory.createObstacle();
|
||||||
|
this.obstacles.push(newObstacle);
|
||||||
|
this.lastDifficultyScore = this.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should add an obstacle based on time
|
||||||
|
const timeDiff = currentTime - this.lastDifficultyTime;
|
||||||
|
if (timeDiff >= GameConfig.DIFFICULTY_TIME_INTERVAL &&
|
||||||
|
this.obstacles.length < GameConfig.MAX_OBSTACLES) {
|
||||||
|
const newObstacle = this.entityFactory.createObstacle();
|
||||||
|
this.obstacles.push(newObstacle);
|
||||||
|
this.lastDifficultyTime = currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load high score from localStorage
|
* Load high score from localStorage
|
||||||
* @returns {number} High score value
|
* @returns {number} High score value
|
||||||
|
|
@ -602,6 +833,11 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.gameActive) {
|
if (this.gameActive) {
|
||||||
|
// Update screen shake
|
||||||
|
if (this.screenShakeTime > 0) {
|
||||||
|
this.screenShakeTime = Math.max(0, this.screenShakeTime - deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
// Update combo timer
|
// Update combo timer
|
||||||
this.comboTimer = Math.max(0, this.comboTimer - deltaTime);
|
this.comboTimer = Math.max(0, this.comboTimer - deltaTime);
|
||||||
if (this.comboTimer <= 0 && this.comboMultiplier > 1) {
|
if (this.comboTimer <= 0 && this.comboMultiplier > 1) {
|
||||||
|
|
@ -620,6 +856,9 @@ export class Game {
|
||||||
this.healthRegenTimer = 0;
|
this.healthRegenTimer = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Difficulty scaling - add obstacles over time
|
||||||
|
this.updateDifficulty(deltaTime);
|
||||||
|
|
||||||
// Update ECS world with actual deltaTime
|
// Update ECS world with actual deltaTime
|
||||||
this.world.update(deltaTime);
|
this.world.update(deltaTime);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,31 @@ export const GameConfig = {
|
||||||
INVINCIBILITY_DURATION: 1.5, // Seconds of invincibility after damage
|
INVINCIBILITY_DURATION: 1.5, // Seconds of invincibility after damage
|
||||||
INVINCIBILITY_FLASH_RATE: 0.1, // Seconds between flash toggles
|
INVINCIBILITY_FLASH_RATE: 0.1, // Seconds between flash toggles
|
||||||
|
|
||||||
|
// Screen Shake
|
||||||
|
SCREEN_SHAKE_DURATION: 0.3, // Seconds of screen shake after damage
|
||||||
|
SCREEN_SHAKE_INTENSITY: 0.5, // Camera shake intensity
|
||||||
|
|
||||||
|
// Particle Effects
|
||||||
|
PARTICLE_COUNT_COIN: 20, // Particles when collecting coin
|
||||||
|
PARTICLE_COUNT_DAMAGE: 15, // Particles when taking damage
|
||||||
|
PARTICLE_LIFETIME: 1.0, // Seconds particles live
|
||||||
|
|
||||||
|
// Power-Ups
|
||||||
|
POWERUP_SPAWN_CHANCE: 0.3, // Chance to spawn power-up instead of coin (30%)
|
||||||
|
POWERUP_DURATION_SPEED: 10, // Speed boost duration
|
||||||
|
POWERUP_DURATION_SHIELD: 15, // Shield duration
|
||||||
|
POWERUP_DURATION_MULTIPLIER: 20, // Score multiplier duration
|
||||||
|
POWERUP_DURATION_MAGNET: 15, // Magnet duration
|
||||||
|
POWERUP_SPEED_MULTIPLIER: 1.5, // Speed boost multiplier
|
||||||
|
POWERUP_SCORE_MULTIPLIER: 2.0, // Score multiplier value
|
||||||
|
POWERUP_MAGNET_RANGE: 5.0, // Magnet attraction range
|
||||||
|
|
||||||
// Difficulty
|
// Difficulty
|
||||||
INITIAL_OBSTACLE_COUNT: 8,
|
INITIAL_OBSTACLE_COUNT: 8,
|
||||||
INITIAL_COIN_COUNT: 10,
|
INITIAL_COIN_COUNT: 10,
|
||||||
|
DIFFICULTY_SCORE_INTERVAL: 100, // Add obstacle every N points
|
||||||
|
DIFFICULTY_TIME_INTERVAL: 30, // Add obstacle every N seconds
|
||||||
|
MAX_OBSTACLES: 20, // Maximum obstacles on screen
|
||||||
|
|
||||||
// Arena
|
// Arena
|
||||||
GROUND_SIZE: 30,
|
GROUND_SIZE: 30,
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,67 @@ import { System } from '../ecs/System.js';
|
||||||
import { ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
import { ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
||||||
import { Transform } from '../components/Transform.js';
|
import { Transform } from '../components/Transform.js';
|
||||||
import { Velocity } from '../components/Velocity.js';
|
import { Velocity } from '../components/Velocity.js';
|
||||||
|
import { ObstacleType } from '../components/ObstacleType.js';
|
||||||
|
import { MeshComponent } from '../components/MeshComponent.js';
|
||||||
|
import { PlayerTag } from '../components/Tags.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ObstacleSystem - handles obstacle-specific behavior
|
* ObstacleSystem - handles obstacle-specific behavior
|
||||||
*/
|
*/
|
||||||
export class ObstacleSystem extends System {
|
export class ObstacleSystem extends System {
|
||||||
update(_deltaTime) {
|
/**
|
||||||
|
* @param {EntityId} [playerEntity=null] - Player entity ID for chasing behavior
|
||||||
|
*/
|
||||||
|
constructor(playerEntity = null) {
|
||||||
|
super();
|
||||||
|
/** @type {EntityId|null} */
|
||||||
|
this.playerEntity = playerEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the player entity for chasing obstacles
|
||||||
|
* @param {EntityId} playerEntity
|
||||||
|
*/
|
||||||
|
setPlayerEntity(playerEntity) {
|
||||||
|
this.playerEntity = playerEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaTime) {
|
||||||
const obstacles = this.getEntities(ObstacleTag, Transform, Velocity, BoundaryConstrained);
|
const obstacles = this.getEntities(ObstacleTag, Transform, Velocity, BoundaryConstrained);
|
||||||
|
|
||||||
|
// Get player position if available
|
||||||
|
let playerPosition = null;
|
||||||
|
if (this.playerEntity) {
|
||||||
|
const playerTransform = this.getComponent(this.playerEntity, Transform);
|
||||||
|
if (playerTransform) {
|
||||||
|
playerPosition = playerTransform.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const entityId of obstacles) {
|
for (const entityId of obstacles) {
|
||||||
const transform = this.getComponent(entityId, Transform);
|
const transform = this.getComponent(entityId, Transform);
|
||||||
const velocity = this.getComponent(entityId, Velocity);
|
const velocity = this.getComponent(entityId, Velocity);
|
||||||
const boundary = this.getComponent(entityId, BoundaryConstrained);
|
const boundary = this.getComponent(entityId, BoundaryConstrained);
|
||||||
|
const obstacleType = this.getComponent(entityId, ObstacleType);
|
||||||
|
const meshComp = this.getComponent(entityId, MeshComponent);
|
||||||
|
|
||||||
|
// Handle different obstacle types
|
||||||
|
if (obstacleType) {
|
||||||
|
// Chasing obstacles - move toward player
|
||||||
|
if (obstacleType.chases && playerPosition) {
|
||||||
|
const direction = playerPosition.clone().sub(transform.position);
|
||||||
|
direction.y = 0;
|
||||||
|
direction.normalize();
|
||||||
|
const chaseSpeed = 0.08 * obstacleType.speedMultiplier;
|
||||||
|
velocity.velocity.x = direction.x * chaseSpeed;
|
||||||
|
velocity.velocity.z = direction.z * chaseSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spinning obstacles - rotate faster
|
||||||
|
if (obstacleType.spins && meshComp) {
|
||||||
|
transform.rotation.y += 6 * obstacleType.rotationSpeed * deltaTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const boundaryLimit = boundary.getBoundary() - 1;
|
const boundaryLimit = boundary.getBoundary() - 1;
|
||||||
|
|
||||||
|
|
|
||||||
106
src/systems/ParticleSystem.js
Normal file
106
src/systems/ParticleSystem.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { System } from '../ecs/System.js';
|
||||||
|
import { Transform } from '../components/Transform.js';
|
||||||
|
import { ParticleEmitter } from '../components/ParticleEmitter.js';
|
||||||
|
import { GameConfig } from '../game/GameConfig.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ParticleSystem - manages particle effects for visual feedback
|
||||||
|
*/
|
||||||
|
export class ParticleSystem extends System {
|
||||||
|
constructor(scene) {
|
||||||
|
super();
|
||||||
|
/** @type {import('three').Scene} */
|
||||||
|
this.scene = scene;
|
||||||
|
|
||||||
|
/** @type {Array<{mesh: import('three').Mesh, velocity: import('three').Vector3, lifetime: number, maxLifetime: number}>} */
|
||||||
|
this.particles = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create particles at a position
|
||||||
|
* @param {import('three').Vector3} position - Position to emit from
|
||||||
|
* @param {number} count - Number of particles
|
||||||
|
* @param {number} color - Color (hex)
|
||||||
|
* @param {number} [speed=5] - Particle speed
|
||||||
|
*/
|
||||||
|
emit(position, count, color, speed = 5) {
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const geometry = new window.THREE.SphereGeometry(0.1, 8, 8);
|
||||||
|
const material = new window.THREE.MeshBasicMaterial({
|
||||||
|
color: color,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 1.0
|
||||||
|
});
|
||||||
|
const mesh = new window.THREE.Mesh(geometry, material);
|
||||||
|
mesh.position.copy(position);
|
||||||
|
this.scene.add(mesh);
|
||||||
|
|
||||||
|
// Random velocity direction
|
||||||
|
const velocity = new window.THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * speed,
|
||||||
|
Math.random() * speed * 0.5 + speed * 0.5,
|
||||||
|
(Math.random() - 0.5) * speed
|
||||||
|
);
|
||||||
|
|
||||||
|
this.particles.push({
|
||||||
|
mesh: mesh,
|
||||||
|
velocity: velocity,
|
||||||
|
lifetime: GameConfig.PARTICLE_LIFETIME,
|
||||||
|
maxLifetime: GameConfig.PARTICLE_LIFETIME
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update particles
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
// Update existing particles
|
||||||
|
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||||
|
const particle = this.particles[i];
|
||||||
|
|
||||||
|
// Update position
|
||||||
|
particle.mesh.position.add(
|
||||||
|
particle.velocity.clone().multiplyScalar(deltaTime)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
particle.velocity.y -= 9.8 * deltaTime;
|
||||||
|
|
||||||
|
// Update lifetime
|
||||||
|
particle.lifetime -= deltaTime;
|
||||||
|
|
||||||
|
// Fade out
|
||||||
|
const alpha = particle.lifetime / particle.maxLifetime;
|
||||||
|
particle.mesh.material.opacity = alpha;
|
||||||
|
|
||||||
|
// Remove dead particles
|
||||||
|
if (particle.lifetime <= 0) {
|
||||||
|
this.scene.remove(particle.mesh);
|
||||||
|
particle.mesh.geometry.dispose();
|
||||||
|
particle.mesh.material.dispose();
|
||||||
|
this.particles.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process particle emitters from entities
|
||||||
|
const entities = this.getEntities(Transform, ParticleEmitter);
|
||||||
|
for (const entityId of entities) {
|
||||||
|
const transform = this.getComponent(entityId, Transform);
|
||||||
|
const emitter = this.getComponent(entityId, ParticleEmitter);
|
||||||
|
|
||||||
|
if (emitter && emitter.active) {
|
||||||
|
this.emit(
|
||||||
|
transform.position,
|
||||||
|
emitter.count,
|
||||||
|
emitter.color,
|
||||||
|
emitter.speed
|
||||||
|
);
|
||||||
|
// Deactivate after emitting once
|
||||||
|
emitter.active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,13 +3,30 @@ import { PlayerTag } from '../components/Tags.js';
|
||||||
import { Velocity } from '../components/Velocity.js';
|
import { Velocity } from '../components/Velocity.js';
|
||||||
import { Transform } from '../components/Transform.js';
|
import { Transform } from '../components/Transform.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('./PowerUpSystem.js').PowerUpSystem} PowerUpSystem
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PlayerControlSystem - handles player input and applies to velocity
|
* PlayerControlSystem - handles player input and applies to velocity
|
||||||
*/
|
*/
|
||||||
export class PlayerControlSystem extends System {
|
export class PlayerControlSystem extends System {
|
||||||
constructor(inputSystem) {
|
/**
|
||||||
|
* @param {InputSystem} inputSystem - Input system for reading controls
|
||||||
|
* @param {PowerUpSystem} [powerUpSystem=null] - Power-up system for speed multiplier
|
||||||
|
*/
|
||||||
|
constructor(inputSystem, powerUpSystem = null) {
|
||||||
super();
|
super();
|
||||||
this.inputSystem = inputSystem;
|
this.inputSystem = inputSystem;
|
||||||
|
this.powerUpSystem = powerUpSystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the power-up system
|
||||||
|
* @param {PowerUpSystem} powerUpSystem
|
||||||
|
*/
|
||||||
|
setPowerUpSystem(powerUpSystem) {
|
||||||
|
this.powerUpSystem = powerUpSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
|
|
@ -19,30 +36,34 @@ export class PlayerControlSystem extends System {
|
||||||
const velocity = this.getComponent(entityId, Velocity);
|
const velocity = this.getComponent(entityId, Velocity);
|
||||||
const transform = this.getComponent(entityId, Transform);
|
const transform = this.getComponent(entityId, Transform);
|
||||||
|
|
||||||
|
// Get speed multiplier from power-up system
|
||||||
|
const speedMultiplier = this.powerUpSystem ? this.powerUpSystem.speedMultiplier : 1.0;
|
||||||
|
const effectiveMaxSpeed = velocity.maxSpeed * speedMultiplier;
|
||||||
|
|
||||||
// Calculate target velocity from input
|
// Calculate target velocity from input
|
||||||
const targetVelocity = new window.THREE.Vector3(0, 0, 0);
|
const targetVelocity = new window.THREE.Vector3(0, 0, 0);
|
||||||
|
|
||||||
// Keyboard input
|
// Keyboard input
|
||||||
if (this.inputSystem.isKeyPressed('w') || this.inputSystem.isKeyPressed('up')) {
|
if (this.inputSystem.isKeyPressed('w') || this.inputSystem.isKeyPressed('up')) {
|
||||||
targetVelocity.z -= velocity.maxSpeed;
|
targetVelocity.z -= effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
if (this.inputSystem.isKeyPressed('s') || this.inputSystem.isKeyPressed('down')) {
|
if (this.inputSystem.isKeyPressed('s') || this.inputSystem.isKeyPressed('down')) {
|
||||||
targetVelocity.z += velocity.maxSpeed;
|
targetVelocity.z += effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
if (this.inputSystem.isKeyPressed('a') || this.inputSystem.isKeyPressed('left')) {
|
if (this.inputSystem.isKeyPressed('a') || this.inputSystem.isKeyPressed('left')) {
|
||||||
targetVelocity.x -= velocity.maxSpeed;
|
targetVelocity.x -= effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
if (this.inputSystem.isKeyPressed('d') || this.inputSystem.isKeyPressed('right')) {
|
if (this.inputSystem.isKeyPressed('d') || this.inputSystem.isKeyPressed('right')) {
|
||||||
targetVelocity.x += velocity.maxSpeed;
|
targetVelocity.x += effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch input
|
// Touch input
|
||||||
const touch = this.inputSystem.getTouchDirection();
|
const touch = this.inputSystem.getTouchDirection();
|
||||||
if (Math.abs(touch.x) > 0.3) {
|
if (Math.abs(touch.x) > 0.3) {
|
||||||
targetVelocity.x = touch.x * velocity.maxSpeed;
|
targetVelocity.x = touch.x * effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
if (Math.abs(touch.y) > 0.3) {
|
if (Math.abs(touch.y) > 0.3) {
|
||||||
targetVelocity.z = touch.y * velocity.maxSpeed;
|
targetVelocity.z = touch.y * effectiveMaxSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply smooth acceleration/deceleration
|
// Apply smooth acceleration/deceleration
|
||||||
|
|
|
||||||
133
src/systems/PowerUpSystem.js
Normal file
133
src/systems/PowerUpSystem.js
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { System } from '../ecs/System.js';
|
||||||
|
import { PowerUp } from '../components/PowerUp.js';
|
||||||
|
import { Transform } from '../components/Transform.js';
|
||||||
|
import { Velocity } from '../components/Velocity.js';
|
||||||
|
import { PlayerTag } from '../components/Tags.js';
|
||||||
|
import { GameConfig } from '../game/GameConfig.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PowerUpSystem - manages active power-up effects on the player
|
||||||
|
*/
|
||||||
|
export class PowerUpSystem extends System {
|
||||||
|
/**
|
||||||
|
* @param {EntityId} [playerEntity=null] - Player entity ID
|
||||||
|
*/
|
||||||
|
constructor(playerEntity = null) {
|
||||||
|
super();
|
||||||
|
/** @type {EntityId|null} */
|
||||||
|
this.playerEntity = playerEntity;
|
||||||
|
|
||||||
|
/** @type {Object<string, PowerUp>} Active power-ups by type */
|
||||||
|
this.activePowerUps = {};
|
||||||
|
|
||||||
|
/** @type {number} Base speed multiplier (1.0 = normal) */
|
||||||
|
this.speedMultiplier = 1.0;
|
||||||
|
|
||||||
|
/** @type {number} Score multiplier (1.0 = normal) */
|
||||||
|
this.scoreMultiplier = 1.0;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether magnet is active */
|
||||||
|
this.magnetActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the player entity
|
||||||
|
* @param {EntityId} playerEntity
|
||||||
|
*/
|
||||||
|
setPlayerEntity(playerEntity) {
|
||||||
|
this.playerEntity = playerEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate a power-up
|
||||||
|
* @param {string} type - Power-up type
|
||||||
|
* @param {number} duration - Duration in seconds
|
||||||
|
*/
|
||||||
|
activatePowerUp(type, duration) {
|
||||||
|
this.activePowerUps[type] = new PowerUp(type, duration);
|
||||||
|
this.updateEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update active power-up effects
|
||||||
|
*/
|
||||||
|
updateEffects() {
|
||||||
|
// Reset all effects
|
||||||
|
this.speedMultiplier = 1.0;
|
||||||
|
this.scoreMultiplier = 1.0;
|
||||||
|
this.magnetActive = false;
|
||||||
|
|
||||||
|
// Apply active power-ups
|
||||||
|
for (const [type, powerUp] of Object.entries(this.activePowerUps)) {
|
||||||
|
if (powerUp.active && powerUp.timeRemaining > 0) {
|
||||||
|
switch (type) {
|
||||||
|
case 'speed':
|
||||||
|
this.speedMultiplier = GameConfig.POWERUP_SPEED_MULTIPLIER;
|
||||||
|
break;
|
||||||
|
case 'multiplier':
|
||||||
|
this.scoreMultiplier = GameConfig.POWERUP_SCORE_MULTIPLIER;
|
||||||
|
break;
|
||||||
|
case 'magnet':
|
||||||
|
this.magnetActive = true;
|
||||||
|
break;
|
||||||
|
case 'shield':
|
||||||
|
// Shield is handled separately in collision system
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a power-up type is active
|
||||||
|
* @param {string} type - Power-up type
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isActive(type) {
|
||||||
|
const powerUp = this.activePowerUps[type];
|
||||||
|
return powerUp && powerUp.active && powerUp.timeRemaining > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update power-ups and apply magnet effect
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
// Update all active power-ups
|
||||||
|
for (const [type, powerUp] of Object.entries(this.activePowerUps)) {
|
||||||
|
if (!powerUp.update(deltaTime)) {
|
||||||
|
// Power-up expired
|
||||||
|
delete this.activePowerUps[type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update effects
|
||||||
|
this.updateEffects();
|
||||||
|
|
||||||
|
// Apply magnet effect - attract coins to player
|
||||||
|
if (this.magnetActive && this.playerEntity) {
|
||||||
|
const playerTransform = this.getComponent(this.playerEntity, Transform);
|
||||||
|
if (!playerTransform) return;
|
||||||
|
|
||||||
|
// Get all coins
|
||||||
|
const coins = this.getEntities(Transform);
|
||||||
|
for (const coinId of coins) {
|
||||||
|
const coinTransform = this.getComponent(coinId, Transform);
|
||||||
|
if (!coinTransform) continue;
|
||||||
|
|
||||||
|
const distance = playerTransform.position.distanceTo(coinTransform.position);
|
||||||
|
|
||||||
|
if (distance < GameConfig.POWERUP_MAGNET_RANGE && distance > 0.5) {
|
||||||
|
// Attract coin to player
|
||||||
|
const direction = playerTransform.position.clone().sub(coinTransform.position);
|
||||||
|
direction.y = 0;
|
||||||
|
direction.normalize();
|
||||||
|
|
||||||
|
const attractSpeed = 0.15;
|
||||||
|
coinTransform.position.add(direction.multiplyScalar(attractSpeed * deltaTime * 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue