import { System } from '../ecs/System.js'; import { Score } from '../components/Score.js'; import { Health } from '../components/Health.js'; import { SoundEvent } from '../components/SoundEvent.js'; import { Collidable } from '../components/Collidable.js'; import { GameConfig } from '../game/GameConfig.js'; /** * GameStateSystem - manages game state (score, combo, health regen, difficulty) * Operates on a singleton Score component */ export class GameStateSystem extends System { constructor(entityFactory) { super(); /** @type {import('../game/EntityFactory.js').EntityFactory} */ this.entityFactory = entityFactory; /** @type {import('../ecs/World.js').EntityId|null} Reference to player entity */ this.playerEntity = null; /** @type {number} Last time obstacle was added */ this.lastDifficultyTime = 0; } /** * Set player entity reference * @param {import('../ecs/World.js').EntityId} entityId */ setPlayerEntity(entityId) { this.playerEntity = entityId; } /** * Initialize - create score entity and load high score */ init() { const scoreEntity = this.world.createEntity(); const score = new Score(); score.highScore = this.loadHighScore(); this.world.addComponent(scoreEntity, score); } /** * Update - manages combo timer, health regen, difficulty scaling * @param {number} deltaTime */ update(deltaTime) { const scoreEntity = this.getScoreEntity(); const score = this.getComponent(scoreEntity, Score); if (!score) return; // Update combo timer score.comboTimer = Math.max(0, score.comboTimer - deltaTime); if (score.comboTimer <= 0 && score.comboMultiplier > 1) { score.comboMultiplier = 1; } // Update health regeneration // Health regen is handled here but could be a separate component // For simplicity, we'll check player health directly if (this.playerEntity) { const health = this.getComponent(this.playerEntity, Health); if (health && health.currentHealth < health.maxHealth) { // Simple approach: heal every interval // In a more complex system, this would be a HealthRegen component const currentTime = performance.now() / 1000; if (!this.lastHealthRegenTime) { this.lastHealthRegenTime = currentTime; } if (currentTime - this.lastHealthRegenTime >= GameConfig.HEALTH_REGEN_INTERVAL) { health.heal(GameConfig.HEALTH_REGEN_AMOUNT); const healthRegenEntity = this.world.createEntity(); this.world.addComponent(healthRegenEntity, new SoundEvent('health')); this.lastHealthRegenTime = currentTime; } } } // Update difficulty scaling const currentTime = performance.now() / 1000; // Check score-based difficulty const obstacles = this.getEntities(Collidable).filter(id => { const collidable = this.getComponent(id, Collidable); return collidable && collidable.layer === 'obstacle'; }); if (score.score - (score.lastDifficultyScore || 0) >= GameConfig.DIFFICULTY_SCORE_INTERVAL && obstacles.length < GameConfig.MAX_OBSTACLES) { this.entityFactory.createObstacle(); score.lastDifficultyScore = score.score; } // Check time-based difficulty if (currentTime - this.lastDifficultyTime >= GameConfig.DIFFICULTY_TIME_INTERVAL) { if (obstacles.length < GameConfig.MAX_OBSTACLES) { this.entityFactory.createObstacle(); this.lastDifficultyTime = currentTime; } } } /** * Reset game state */ reset() { const scoreEntity = this.getScoreEntity(); const score = this.getComponent(scoreEntity, Score); if (score) { score.score = 0; score.comboMultiplier = 1; score.comboTimer = 0; score.lastCoinTime = 0; score.lastDifficultyScore = 0; } this.lastDifficultyTime = 0; this.lastHealthRegenTime = 0; } /** * Get score entity (singleton) * @private */ getScoreEntity() { const scoreEntities = this.getEntities(Score); if (scoreEntities.length > 0) { return scoreEntities[0]; } // Create if doesn't exist const entity = this.world.createEntity(); this.world.addComponent(entity, new Score()); return entity; } /** * Load high score from localStorage * @private */ loadHighScore() { try { const saved = localStorage.getItem(GameConfig.STORAGE_HIGH_SCORE); return saved ? parseInt(saved, 10) : 0; } catch (error) { console.debug('Failed to load high score:', error); return 0; } } }