diff --git a/nginx.conf b/nginx.conf index eee0bb3..2c3229d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -9,27 +9,6 @@ server { try_files $uri $uri/ =404; } - # Prevent caching of JavaScript files and version.json - location ~* \.(js|mjs)$ { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - add_header Vary "Accept-Encoding"; - } - - location = /version.json { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - - # Prevent caching of HTML - location ~* \.html$ { - add_header Cache-Control "no-cache, no-store, must-revalidate"; - add_header Pragma "no-cache"; - add_header Expires "0"; - } - # Enable gzip compression gzip on; gzip_types text/html text/css application/javascript; diff --git a/src/components/CoinType.js b/src/components/CoinType.js deleted file mode 100644 index fc989e6..0000000 --- a/src/components/CoinType.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 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; - } - } -} - diff --git a/src/components/ObstacleType.js b/src/components/ObstacleType.js deleted file mode 100644 index c003531..0000000 --- a/src/components/ObstacleType.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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; - } -} - diff --git a/src/components/ParticleEmitter.js b/src/components/ParticleEmitter.js deleted file mode 100644 index 8afac89..0000000 --- a/src/components/ParticleEmitter.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * 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; - } -} - diff --git a/src/components/PowerUp.js b/src/components/PowerUp.js deleted file mode 100644 index adec6df..0000000 --- a/src/components/PowerUp.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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; - } -} - diff --git a/src/game/EntityFactory.js b/src/game/EntityFactory.js index 0b0c505..f398994 100644 --- a/src/game/EntityFactory.js +++ b/src/game/EntityFactory.js @@ -4,9 +4,6 @@ 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'; @@ -68,58 +65,19 @@ export class EntityFactory { /** * 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) { + createCoin(index = 0) { 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); + // Create mesh + const geometry = new window.THREE.SphereGeometry(0.3, 16, 16); const material = new window.THREE.MeshStandardMaterial({ - color: color, + color: 0xFFD700, metalness: 0.8, roughness: 0.2, - emissive: emissive, - emissiveIntensity: emissiveIntensity + emissive: 0xFFD700, + emissiveIntensity: 0.3 }); const mesh = new window.THREE.Mesh(geometry, material); mesh.castShadow = true; @@ -134,7 +92,6 @@ export class EntityFactory { 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; @@ -142,42 +99,15 @@ export class EntityFactory { /** * Create an obstacle entity - * @param {string} [type] - Optional obstacle type ('normal', 'fast', 'chasing', 'spinning') * @returns {EntityId} The obstacle entity ID */ - createObstacle(type = null) { + createObstacle() { 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 + // Create mesh 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, + color: 0xFF4500, metalness: 0.3, roughness: 0.7 }); @@ -193,12 +123,11 @@ export class EntityFactory { 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; + // Random velocity const velocity = new Velocity( - (Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier, + (Math.random() - 0.5) * 0.05, 0, - (Math.random() - 0.5) * baseSpeed * typeComponent.speedMultiplier + (Math.random() - 0.5) * 0.05 ); // Add components @@ -206,89 +135,12 @@ export class EntityFactory { 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 diff --git a/src/game/Game.js b/src/game/Game.js index 897b46a..76aef4c 100644 --- a/src/game/Game.js +++ b/src/game/Game.js @@ -11,16 +11,12 @@ import { CoinSystem } from '../systems/CoinSystem.js'; import { ObstacleSystem } from '../systems/ObstacleSystem.js'; import { CollisionSystem } from '../systems/CollisionSystem.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'; // Components import { Transform } from '../components/Transform.js'; import { Health } from '../components/Health.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. @@ -50,9 +46,6 @@ export class Game { /** @type {EntityId[]} Array of obstacle entity IDs */ this.obstacles = []; - - /** @type {EntityId[]} Array of power-up entity IDs */ - this.powerUps = []; /** @type {number} Last frame timestamp for deltaTime calculation */ this.lastTime = performance.now(); @@ -96,23 +89,6 @@ export class Game { /** @type {number} Time since last health regeneration */ 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.setupEventListeners(); this.animate(); @@ -210,17 +186,13 @@ export class Game { this.inputSystem = new InputSystem(); this.world.addSystem(this.inputSystem); - // Player control (will set power-up system after it's created) - this.playerControlSystem = new PlayerControlSystem(this.inputSystem); - this.world.addSystem(this.playerControlSystem); + // Player control + this.world.addSystem(new PlayerControlSystem(this.inputSystem)); // Movement and physics this.world.addSystem(new MovementSystem()); this.world.addSystem(new BoundarySystem()); - - // Obstacle system (will set player entity after player is created) - this.obstacleSystem = new ObstacleSystem(); - this.world.addSystem(this.obstacleSystem); + this.world.addSystem(new ObstacleSystem()); // Game-specific behavior this.world.addSystem(new CoinSystem()); @@ -228,19 +200,6 @@ export class Game { // Invincibility system (before collision to update state) 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 this.collisionSystem = new CollisionSystem(); this.collisionSystem.onCollision((entity1, entity2, layer1, layer2) => { @@ -255,14 +214,6 @@ export class Game { createGameEntities() { // Create player 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 for (let i = 0; i < GameConfig.INITIAL_COIN_COUNT; i++) { @@ -270,7 +221,7 @@ export class Game { this.coins.push(coin); } - // Create obstacles (mix of types) + // Create obstacles for (let i = 0; i < GameConfig.INITIAL_OBSTACLE_COUNT; i++) { const obstacle = this.entityFactory.createObstacle(); this.obstacles.push(obstacle); @@ -286,12 +237,6 @@ export class Game { 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 if ((layer1 === 'player' && layer2 === 'obstacle') || (layer1 === 'obstacle' && layer2 === 'player')) { const playerEntity = layer1 === 'player' ? entity1 : entity2; @@ -301,11 +246,6 @@ export class Game { } 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 this.entityFactory.destroyEntity(coinEntity); const index = this.coins.indexOf(coinEntity); @@ -313,42 +253,7 @@ export class Game { this.coins.splice(index, 1); } - // 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) + // Update combo system const currentTime = performance.now() / 1000; // Convert to seconds const timeSinceLastCoin = currentTime - this.lastCoinTime; @@ -367,10 +272,9 @@ export class Game { this.lastCoinTime = currentTime; - // Calculate score with combo multiplier and power-up multiplier (use coin's base value) - const baseScore = coinType ? coinType.scoreValue : GameConfig.COMBO_BASE_SCORE; - const powerUpMultiplier = this.powerUpSystem ? this.powerUpSystem.scoreMultiplier : 1.0; - const scoreGain = baseScore * this.comboMultiplier * powerUpMultiplier; + // Calculate score with combo multiplier + const baseScore = GameConfig.COMBO_BASE_SCORE; + const scoreGain = baseScore * this.comboMultiplier; this.score += scoreGain; // Check for new high score @@ -381,85 +285,12 @@ export class Game { this.updateUI(); - // Spawn new coin or power-up (based on chance) - if (Math.random() < GameConfig.POWERUP_SPAWN_CHANCE) { - 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); - } + // Spawn new coin + const newCoin = this.entityFactory.createCoin(this.coins.length); + this.coins.push(newCoin); } 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 const invincibility = this.world.getComponent(playerEntity, Invincibility); if (invincibility && invincibility.getIsInvincible()) { @@ -478,19 +309,6 @@ export class Game { 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 const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position); pushDirection.y = 0; @@ -514,20 +332,8 @@ export class Game { const playerTransform = this.world.getComponent(this.playerEntity, Transform); if (playerTransform) { - // Base camera position - 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.position.x = playerTransform.position.x; + this.camera.position.z = playerTransform.position.z + 15; this.camera.lookAt(playerTransform.position); } } @@ -577,14 +383,12 @@ export class Game { // Clean up old entities [...this.coins].forEach(coin => this.entityFactory.destroyEntity(coin)); [...this.obstacles].forEach(obstacle => this.entityFactory.destroyEntity(obstacle)); - [...this.powerUps].forEach(powerUp => this.entityFactory.destroyEntity(powerUp)); if (this.playerEntity) { this.entityFactory.destroyEntity(this.playerEntity); } this.coins = []; this.obstacles = []; - this.powerUps = []; // Reset game state this.score = 0; @@ -598,14 +402,6 @@ export class Game { // Reset health regeneration timer 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 this.createGameEntities(); @@ -713,11 +509,7 @@ export class Game { } loadVersion() { - // Add cache-busting query parameter to ensure fresh version data - const cacheBuster = `?t=${Date.now()}`; - fetch(`/version.json${cacheBuster}`, { - cache: 'no-store' - }) + fetch('/version.json') .then(response => { if (response.ok) { return response.json(); @@ -735,33 +527,6 @@ 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 * @returns {number} High score value @@ -833,11 +598,6 @@ export class Game { } if (this.gameActive) { - // Update screen shake - if (this.screenShakeTime > 0) { - this.screenShakeTime = Math.max(0, this.screenShakeTime - deltaTime); - } - // Update combo timer this.comboTimer = Math.max(0, this.comboTimer - deltaTime); if (this.comboTimer <= 0 && this.comboMultiplier > 1) { @@ -856,9 +616,6 @@ export class Game { this.healthRegenTimer = 0; } - // Difficulty scaling - add obstacles over time - this.updateDifficulty(deltaTime); - // Update ECS world with actual deltaTime this.world.update(deltaTime); diff --git a/src/game/GameConfig.js b/src/game/GameConfig.js index f9b0ee1..3fd6de1 100644 --- a/src/game/GameConfig.js +++ b/src/game/GameConfig.js @@ -18,31 +18,9 @@ export const GameConfig = { INVINCIBILITY_DURATION: 1.5, // Seconds of invincibility after damage 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 INITIAL_OBSTACLE_COUNT: 8, 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 GROUND_SIZE: 30, diff --git a/src/systems/ObstacleSystem.js b/src/systems/ObstacleSystem.js index 263a93b..92826a6 100644 --- a/src/systems/ObstacleSystem.js +++ b/src/systems/ObstacleSystem.js @@ -2,67 +2,18 @@ import { System } from '../ecs/System.js'; import { ObstacleTag, BoundaryConstrained } from '../components/Tags.js'; import { Transform } from '../components/Transform.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 */ export class ObstacleSystem extends System { - /** - * @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) { + update(_deltaTime) { 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) { const transform = this.getComponent(entityId, Transform); const velocity = this.getComponent(entityId, Velocity); 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; diff --git a/src/systems/ParticleSystem.js b/src/systems/ParticleSystem.js deleted file mode 100644 index d1f2e7d..0000000 --- a/src/systems/ParticleSystem.js +++ /dev/null @@ -1,106 +0,0 @@ -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; - } - } - } -} - diff --git a/src/systems/PlayerControlSystem.js b/src/systems/PlayerControlSystem.js index f6b75df..5c71304 100644 --- a/src/systems/PlayerControlSystem.js +++ b/src/systems/PlayerControlSystem.js @@ -3,30 +3,13 @@ import { PlayerTag } from '../components/Tags.js'; import { Velocity } from '../components/Velocity.js'; import { Transform } from '../components/Transform.js'; -/** - * @typedef {import('./PowerUpSystem.js').PowerUpSystem} PowerUpSystem - */ - /** * PlayerControlSystem - handles player input and applies to velocity */ export class PlayerControlSystem extends System { - /** - * @param {InputSystem} inputSystem - Input system for reading controls - * @param {PowerUpSystem} [powerUpSystem=null] - Power-up system for speed multiplier - */ - constructor(inputSystem, powerUpSystem = null) { + constructor(inputSystem) { super(); this.inputSystem = inputSystem; - this.powerUpSystem = powerUpSystem; - } - - /** - * Set the power-up system - * @param {PowerUpSystem} powerUpSystem - */ - setPowerUpSystem(powerUpSystem) { - this.powerUpSystem = powerUpSystem; } update(deltaTime) { @@ -36,34 +19,30 @@ export class PlayerControlSystem extends System { const velocity = this.getComponent(entityId, Velocity); 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 const targetVelocity = new window.THREE.Vector3(0, 0, 0); // Keyboard input if (this.inputSystem.isKeyPressed('w') || this.inputSystem.isKeyPressed('up')) { - targetVelocity.z -= effectiveMaxSpeed; + targetVelocity.z -= velocity.maxSpeed; } if (this.inputSystem.isKeyPressed('s') || this.inputSystem.isKeyPressed('down')) { - targetVelocity.z += effectiveMaxSpeed; + targetVelocity.z += velocity.maxSpeed; } if (this.inputSystem.isKeyPressed('a') || this.inputSystem.isKeyPressed('left')) { - targetVelocity.x -= effectiveMaxSpeed; + targetVelocity.x -= velocity.maxSpeed; } if (this.inputSystem.isKeyPressed('d') || this.inputSystem.isKeyPressed('right')) { - targetVelocity.x += effectiveMaxSpeed; + targetVelocity.x += velocity.maxSpeed; } // Touch input const touch = this.inputSystem.getTouchDirection(); if (Math.abs(touch.x) > 0.3) { - targetVelocity.x = touch.x * effectiveMaxSpeed; + targetVelocity.x = touch.x * velocity.maxSpeed; } if (Math.abs(touch.y) > 0.3) { - targetVelocity.z = touch.y * effectiveMaxSpeed; + targetVelocity.z = touch.y * velocity.maxSpeed; } // Apply smooth acceleration/deceleration diff --git a/src/systems/PowerUpSystem.js b/src/systems/PowerUpSystem.js deleted file mode 100644 index 5e4468a..0000000 --- a/src/systems/PowerUpSystem.js +++ /dev/null @@ -1,133 +0,0 @@ -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} 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)); - } - } - } - } -} -