- Added start menu, pause menu, and game over screens to enhance user experience. - Introduced UISystem to manage UI state transitions and visibility. - Updated Game class to handle game state (menu, playing, paused, game over). - Integrated CameraSystem for improved camera control and screen shake effects. - Added new components for collision handling, scoring, and game state management. - Refactored sound management to separate background music handling. This update significantly improves the game's UI and overall gameplay flow. Reviewed-on: #18 Co-authored-by: Juan Sebastian Montoya <juansmm@outlook.com> Co-committed-by: Juan Sebastian Montoya <juansmm@outlook.com>
151 lines
4.6 KiB
JavaScript
151 lines
4.6 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
}
|
|
|