threejs-test/src/systems/GameStateSystem.js
Juan Sebastian Montoya d8f4604e69
All checks were successful
Build and Publish Docker Image / Publish to Registry (push) Successful in 8s
Build and Publish Docker Image / Deploy to Portainer (push) Successful in 1s
Release/First full release version (#18)
- 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>
2025-11-26 18:47:41 -05:00

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;
}
}
}