Feature/Implement invincibility and combo systems (#16)
- Added Invincibility component to manage invincibility state and duration. - Introduced InvincibilitySystem to handle visual effects during invincibility. - Updated Game class to integrate combo multiplier mechanics and high score tracking. - Enhanced UI to display current combo status and high score. - Configured GameConfig for centralized game settings, including obstacle damage and invincibility duration. - Updated game logic to reset combo on damage and manage health regeneration. This update enhances gameplay dynamics by introducing invincibility frames and a scoring combo system. Reviewed-on: #16 Co-authored-by: Juan Sebastian Montoya <juansmm@outlook.com> Co-committed-by: Juan Sebastian Montoya <juansmm@outlook.com>
This commit is contained in:
parent
e33f5a97a7
commit
112aa68a83
6 changed files with 313 additions and 7 deletions
21
index.html
21
index.html
|
|
@ -24,6 +24,23 @@
|
||||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
#combo {
|
||||||
|
position: absolute;
|
||||||
|
top: 100px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
color: #FFD700;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||||
|
z-index: 100;
|
||||||
|
display: none;
|
||||||
|
animation: comboPulse 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
@keyframes comboPulse {
|
||||||
|
0%, 100% { transform: translateX(-50%) scale(1); }
|
||||||
|
50% { transform: translateX(-50%) scale(1.2); }
|
||||||
|
}
|
||||||
#gameOver {
|
#gameOver {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|
@ -119,9 +136,12 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="ui">
|
<div id="ui">
|
||||||
<div>Score: <span id="score">0</span></div>
|
<div>Score: <span id="score">0</span></div>
|
||||||
|
<div>High Score: <span id="highScore">0</span></div>
|
||||||
<div>Health: <span id="health">100</span></div>
|
<div>Health: <span id="health">100</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="combo">1x COMBO!</div>
|
||||||
|
|
||||||
<div id="version">v<span id="versionNumber">-</span></div>
|
<div id="version">v<span id="versionNumber">-</span></div>
|
||||||
|
|
||||||
<div id="perfMonitor">
|
<div id="perfMonitor">
|
||||||
|
|
@ -132,6 +152,7 @@
|
||||||
|
|
||||||
<div id="gameOver">
|
<div id="gameOver">
|
||||||
<h1>Game Over!</h1>
|
<h1>Game Over!</h1>
|
||||||
|
<p id="newHighScore" style="display: none; color: #FFD700; font-size: 28px; margin-bottom: 10px;">🏆 New High Score! 🏆</p>
|
||||||
<p>Final Score: <span id="finalScore">0</span></p>
|
<p>Final Score: <span id="finalScore">0</span></p>
|
||||||
<button id="restartBtn">Play Again</button>
|
<button id="restartBtn">Play Again</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
57
src/components/Invincibility.js
Normal file
57
src/components/Invincibility.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Invincibility component - tracks invincibility state and timer
|
||||||
|
*/
|
||||||
|
export class Invincibility {
|
||||||
|
/**
|
||||||
|
* @param {number} [duration=1.5] - Duration of invincibility in seconds
|
||||||
|
* @param {boolean} [startActive=false] - Whether to start invincible (default false)
|
||||||
|
*/
|
||||||
|
constructor(duration = 1.5, startActive = false) {
|
||||||
|
/** @type {number} Time remaining in seconds */
|
||||||
|
this.timeRemaining = startActive ? duration : 0;
|
||||||
|
|
||||||
|
/** @type {number} Total duration */
|
||||||
|
this.duration = duration;
|
||||||
|
|
||||||
|
/** @type {boolean} Whether entity is currently invincible */
|
||||||
|
this.isInvincible = startActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update invincibility timer
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
* @returns {boolean} True if still invincible
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
if (!this.isInvincible) return false;
|
||||||
|
|
||||||
|
this.timeRemaining -= deltaTime;
|
||||||
|
|
||||||
|
if (this.timeRemaining <= 0) {
|
||||||
|
this.isInvincible = false;
|
||||||
|
this.timeRemaining = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate invincibility
|
||||||
|
* @param {number} [duration] - Optional custom duration
|
||||||
|
*/
|
||||||
|
activate(duration = null) {
|
||||||
|
this.duration = duration || this.duration;
|
||||||
|
this.timeRemaining = this.duration;
|
||||||
|
this.isInvincible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if currently invincible
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
getIsInvincible() {
|
||||||
|
return this.isInvincible && this.timeRemaining > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,7 +3,9 @@ import { Velocity } from '../components/Velocity.js';
|
||||||
import { MeshComponent } from '../components/MeshComponent.js';
|
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 { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
import { PlayerTag, CoinTag, ObstacleTag, BoundaryConstrained } from '../components/Tags.js';
|
||||||
|
import { GameConfig } from './GameConfig.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntityFactory - creates pre-configured game entities with appropriate components.
|
* EntityFactory - creates pre-configured game entities with appropriate components.
|
||||||
|
|
@ -52,6 +54,8 @@ export class EntityFactory {
|
||||||
this.world.addComponent(entity, new MeshComponent(mesh));
|
this.world.addComponent(entity, new MeshComponent(mesh));
|
||||||
this.world.addComponent(entity, new Collidable(0, 'player')); // Player center point (original behavior)
|
this.world.addComponent(entity, new Collidable(0, 'player')); // Player center point (original behavior)
|
||||||
this.world.addComponent(entity, new Health(100));
|
this.world.addComponent(entity, new Health(100));
|
||||||
|
// Invincibility starts inactive until first damage
|
||||||
|
this.world.addComponent(entity, new Invincibility(GameConfig.INVINCIBILITY_DURATION, false));
|
||||||
this.world.addComponent(entity, new PlayerTag());
|
this.world.addComponent(entity, new PlayerTag());
|
||||||
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
this.world.addComponent(entity, new BoundaryConstrained(this.groundSize));
|
||||||
|
|
||||||
|
|
|
||||||
158
src/game/Game.js
158
src/game/Game.js
|
|
@ -1,5 +1,6 @@
|
||||||
import { World } from '../ecs/World.js';
|
import { World } from '../ecs/World.js';
|
||||||
import { EntityFactory } from './EntityFactory.js';
|
import { EntityFactory } from './EntityFactory.js';
|
||||||
|
import { GameConfig } from './GameConfig.js';
|
||||||
|
|
||||||
// Systems
|
// Systems
|
||||||
import { InputSystem } from '../systems/InputSystem.js';
|
import { InputSystem } from '../systems/InputSystem.js';
|
||||||
|
|
@ -9,11 +10,13 @@ import { BoundarySystem } from '../systems/BoundarySystem.js';
|
||||||
import { CoinSystem } from '../systems/CoinSystem.js';
|
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 { 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Game class - manages the game loop and coordinates all systems.
|
* Main Game class - manages the game loop and coordinates all systems.
|
||||||
|
|
@ -24,11 +27,14 @@ import { Health } from '../components/Health.js';
|
||||||
export class Game {
|
export class Game {
|
||||||
constructor() {
|
constructor() {
|
||||||
/** @type {number} Size of the game play area */
|
/** @type {number} Size of the game play area */
|
||||||
this.groundSize = 30;
|
this.groundSize = GameConfig.GROUND_SIZE;
|
||||||
|
|
||||||
/** @type {number} Current game score */
|
/** @type {number} Current game score */
|
||||||
this.score = 0;
|
this.score = 0;
|
||||||
|
|
||||||
|
/** @type {number} High score (loaded from localStorage) */
|
||||||
|
this.highScore = this.loadHighScore();
|
||||||
|
|
||||||
/** @type {boolean} Whether the game is currently active */
|
/** @type {boolean} Whether the game is currently active */
|
||||||
this.gameActive = true;
|
this.gameActive = true;
|
||||||
|
|
||||||
|
|
@ -69,6 +75,20 @@ export class Game {
|
||||||
lastShakeTime: 0
|
lastShakeTime: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Combo system state
|
||||||
|
/** @type {number} Current combo multiplier (1x, 2x, 3x, etc.) */
|
||||||
|
this.comboMultiplier = 1;
|
||||||
|
|
||||||
|
/** @type {number} Time since last coin collection (for combo) */
|
||||||
|
this.comboTimer = 0;
|
||||||
|
|
||||||
|
/** @type {number} Last time a coin was collected */
|
||||||
|
this.lastCoinTime = 0;
|
||||||
|
|
||||||
|
// Health regeneration state
|
||||||
|
/** @type {number} Time since last health regeneration */
|
||||||
|
this.healthRegenTimer = 0;
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
this.animate();
|
this.animate();
|
||||||
|
|
@ -177,6 +197,9 @@ export class Game {
|
||||||
// Game-specific behavior
|
// Game-specific behavior
|
||||||
this.world.addSystem(new CoinSystem());
|
this.world.addSystem(new CoinSystem());
|
||||||
|
|
||||||
|
// Invincibility system (before collision to update state)
|
||||||
|
this.world.addSystem(new InvincibilitySystem());
|
||||||
|
|
||||||
// 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) => {
|
||||||
|
|
@ -193,13 +216,13 @@ export class Game {
|
||||||
this.playerEntity = this.entityFactory.createPlayer();
|
this.playerEntity = this.entityFactory.createPlayer();
|
||||||
|
|
||||||
// Create coins
|
// Create coins
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < GameConfig.INITIAL_COIN_COUNT; i++) {
|
||||||
const coin = this.entityFactory.createCoin(this.coins.length);
|
const coin = this.entityFactory.createCoin(this.coins.length);
|
||||||
this.coins.push(coin);
|
this.coins.push(coin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create obstacles
|
// Create obstacles
|
||||||
for (let i = 0; i < 8; 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);
|
||||||
}
|
}
|
||||||
|
|
@ -230,8 +253,36 @@ export class Game {
|
||||||
this.coins.splice(index, 1);
|
this.coins.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update score
|
// Update combo system
|
||||||
this.score += 10;
|
const currentTime = performance.now() / 1000; // Convert to seconds
|
||||||
|
const timeSinceLastCoin = currentTime - this.lastCoinTime;
|
||||||
|
|
||||||
|
if (timeSinceLastCoin <= GameConfig.COMBO_TIME_WINDOW && this.lastCoinTime > 0) {
|
||||||
|
// Maintain combo
|
||||||
|
this.comboMultiplier = Math.min(
|
||||||
|
this.comboMultiplier + 1,
|
||||||
|
GameConfig.COMBO_MULTIPLIER_MAX
|
||||||
|
);
|
||||||
|
this.comboTimer = GameConfig.COMBO_TIME_WINDOW;
|
||||||
|
} else {
|
||||||
|
// Reset combo
|
||||||
|
this.comboMultiplier = 1;
|
||||||
|
this.comboTimer = GameConfig.COMBO_TIME_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastCoinTime = currentTime;
|
||||||
|
|
||||||
|
// Calculate score with combo multiplier
|
||||||
|
const baseScore = GameConfig.COMBO_BASE_SCORE;
|
||||||
|
const scoreGain = baseScore * this.comboMultiplier;
|
||||||
|
this.score += scoreGain;
|
||||||
|
|
||||||
|
// Check for new high score
|
||||||
|
if (this.score > this.highScore) {
|
||||||
|
this.highScore = this.score;
|
||||||
|
this.saveHighScore(this.highScore);
|
||||||
|
}
|
||||||
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
// Spawn new coin
|
// Spawn new coin
|
||||||
|
|
@ -240,12 +291,23 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleObstacleCollision(playerEntity, obstacleEntity) {
|
handleObstacleCollision(playerEntity, obstacleEntity) {
|
||||||
|
// Check if player is invincible
|
||||||
|
const invincibility = this.world.getComponent(playerEntity, Invincibility);
|
||||||
|
if (invincibility && invincibility.getIsInvincible()) {
|
||||||
|
return; // No damage if invincible
|
||||||
|
}
|
||||||
|
|
||||||
const health = this.world.getComponent(playerEntity, Health);
|
const health = this.world.getComponent(playerEntity, Health);
|
||||||
const playerTransform = this.world.getComponent(playerEntity, Transform);
|
const playerTransform = this.world.getComponent(playerEntity, Transform);
|
||||||
const obstacleTransform = this.world.getComponent(obstacleEntity, Transform);
|
const obstacleTransform = this.world.getComponent(obstacleEntity, Transform);
|
||||||
|
|
||||||
// Damage player
|
// Damage player (using config damage amount)
|
||||||
const isDead = health.damage(1);
|
const isDead = health.damage(GameConfig.OBSTACLE_DAMAGE);
|
||||||
|
|
||||||
|
// Activate invincibility frames
|
||||||
|
if (invincibility) {
|
||||||
|
invincibility.activate(GameConfig.INVINCIBILITY_DURATION);
|
||||||
|
}
|
||||||
|
|
||||||
// Push player back
|
// Push player back
|
||||||
const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position);
|
const pushDirection = playerTransform.position.clone().sub(obstacleTransform.position);
|
||||||
|
|
@ -254,6 +316,10 @@ export class Game {
|
||||||
playerTransform.position.add(pushDirection.multiplyScalar(0.3));
|
playerTransform.position.add(pushDirection.multiplyScalar(0.3));
|
||||||
playerTransform.position.y = 0.5;
|
playerTransform.position.y = 0.5;
|
||||||
|
|
||||||
|
// Reset combo on damage
|
||||||
|
this.comboMultiplier = 1;
|
||||||
|
this.comboTimer = 0;
|
||||||
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
if (isDead) {
|
if (isDead) {
|
||||||
|
|
@ -275,6 +341,23 @@ export class Game {
|
||||||
updateUI() {
|
updateUI() {
|
||||||
document.getElementById('score').textContent = this.score;
|
document.getElementById('score').textContent = this.score;
|
||||||
|
|
||||||
|
// Update high score
|
||||||
|
const highScoreEl = document.getElementById('highScore');
|
||||||
|
if (highScoreEl) {
|
||||||
|
highScoreEl.textContent = this.highScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update combo display
|
||||||
|
const comboEl = document.getElementById('combo');
|
||||||
|
if (comboEl) {
|
||||||
|
if (this.comboMultiplier > 1 && this.comboTimer > 0) {
|
||||||
|
comboEl.textContent = `${this.comboMultiplier}x COMBO!`;
|
||||||
|
comboEl.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
comboEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const health = this.world.getComponent(this.playerEntity, Health);
|
const health = this.world.getComponent(this.playerEntity, Health);
|
||||||
if (health) {
|
if (health) {
|
||||||
document.getElementById('health').textContent = Math.max(0, health.currentHealth);
|
document.getElementById('health').textContent = Math.max(0, health.currentHealth);
|
||||||
|
|
@ -284,6 +367,15 @@ export class Game {
|
||||||
gameOver() {
|
gameOver() {
|
||||||
this.gameActive = false;
|
this.gameActive = false;
|
||||||
document.getElementById('finalScore').textContent = this.score;
|
document.getElementById('finalScore').textContent = this.score;
|
||||||
|
|
||||||
|
// Show "New High Score!" if applicable
|
||||||
|
const newHighScoreEl = document.getElementById('newHighScore');
|
||||||
|
if (newHighScoreEl && this.score === this.highScore && this.score > 0) {
|
||||||
|
newHighScoreEl.style.display = 'block';
|
||||||
|
} else if (newHighScoreEl) {
|
||||||
|
newHighScoreEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('gameOver').style.display = 'block';
|
document.getElementById('gameOver').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,6 +395,14 @@ export class Game {
|
||||||
this.gameActive = true;
|
this.gameActive = true;
|
||||||
this.lastTime = performance.now(); // Reset timer to prevent deltaTime spike
|
this.lastTime = performance.now(); // Reset timer to prevent deltaTime spike
|
||||||
|
|
||||||
|
// Reset combo system
|
||||||
|
this.comboMultiplier = 1;
|
||||||
|
this.comboTimer = 0;
|
||||||
|
this.lastCoinTime = 0;
|
||||||
|
|
||||||
|
// Reset health regeneration timer
|
||||||
|
this.healthRegenTimer = 0;
|
||||||
|
|
||||||
// Recreate entities
|
// Recreate entities
|
||||||
this.createGameEntities();
|
this.createGameEntities();
|
||||||
|
|
||||||
|
|
@ -427,6 +527,32 @@ export class Game {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load high score from localStorage
|
||||||
|
* @returns {number} High score value
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save high score to localStorage
|
||||||
|
* @param {number} score - Score to save
|
||||||
|
*/
|
||||||
|
saveHighScore(score) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(GameConfig.STORAGE_HIGH_SCORE, score.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('Failed to save high score:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onWindowResize() {
|
onWindowResize() {
|
||||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
this.camera.updateProjectionMatrix();
|
this.camera.updateProjectionMatrix();
|
||||||
|
|
@ -472,6 +598,24 @@ export class Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.gameActive) {
|
if (this.gameActive) {
|
||||||
|
// Update combo timer
|
||||||
|
this.comboTimer = Math.max(0, this.comboTimer - deltaTime);
|
||||||
|
if (this.comboTimer <= 0 && this.comboMultiplier > 1) {
|
||||||
|
this.comboMultiplier = 1;
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update health regeneration
|
||||||
|
this.healthRegenTimer += deltaTime;
|
||||||
|
if (this.healthRegenTimer >= GameConfig.HEALTH_REGEN_INTERVAL) {
|
||||||
|
const health = this.world.getComponent(this.playerEntity, Health);
|
||||||
|
if (health && health.currentHealth < health.maxHealth) {
|
||||||
|
health.heal(GameConfig.HEALTH_REGEN_AMOUNT);
|
||||||
|
this.updateUI();
|
||||||
|
}
|
||||||
|
this.healthRegenTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Update ECS world with actual deltaTime
|
// Update ECS world with actual deltaTime
|
||||||
this.world.update(deltaTime);
|
this.world.update(deltaTime);
|
||||||
|
|
||||||
|
|
|
||||||
31
src/game/GameConfig.js
Normal file
31
src/game/GameConfig.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* GameConfig - Centralized game configuration constants
|
||||||
|
* Easy to tweak difficulty, balance, and gameplay parameters
|
||||||
|
*/
|
||||||
|
export const GameConfig = {
|
||||||
|
// Gameplay
|
||||||
|
OBSTACLE_DAMAGE: 3, // Increased from 1 to balance invincibility frames and health regen
|
||||||
|
COIN_SCORE: 10,
|
||||||
|
HEALTH_REGEN_INTERVAL: 10, // Heal 1 HP every 10 seconds
|
||||||
|
HEALTH_REGEN_AMOUNT: 1,
|
||||||
|
|
||||||
|
// Combo System
|
||||||
|
COMBO_TIME_WINDOW: 3, // Seconds between coin collections to maintain combo
|
||||||
|
COMBO_MULTIPLIER_MAX: 5, // Maximum combo multiplier (5x)
|
||||||
|
COMBO_BASE_SCORE: 10, // Base score per coin
|
||||||
|
|
||||||
|
// Invincibility
|
||||||
|
INVINCIBILITY_DURATION: 1.5, // Seconds of invincibility after damage
|
||||||
|
INVINCIBILITY_FLASH_RATE: 0.1, // Seconds between flash toggles
|
||||||
|
|
||||||
|
// Difficulty
|
||||||
|
INITIAL_OBSTACLE_COUNT: 8,
|
||||||
|
INITIAL_COIN_COUNT: 10,
|
||||||
|
|
||||||
|
// Arena
|
||||||
|
GROUND_SIZE: 30,
|
||||||
|
|
||||||
|
// Storage Keys
|
||||||
|
STORAGE_HIGH_SCORE: 'threejs-game-highscore'
|
||||||
|
};
|
||||||
|
|
||||||
49
src/systems/InvincibilitySystem.js
Normal file
49
src/systems/InvincibilitySystem.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { System } from '../ecs/System.js';
|
||||||
|
import { Invincibility } from '../components/Invincibility.js';
|
||||||
|
import { MeshComponent } from '../components/MeshComponent.js';
|
||||||
|
import { GameConfig } from '../game/GameConfig.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InvincibilitySystem - handles invincibility flashing effect
|
||||||
|
*/
|
||||||
|
export class InvincibilitySystem extends System {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.flashTimer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update invincibility timers and flash effect
|
||||||
|
* @param {number} deltaTime - Time since last frame in seconds
|
||||||
|
*/
|
||||||
|
update(deltaTime) {
|
||||||
|
const entities = this.getEntities(Invincibility, MeshComponent);
|
||||||
|
|
||||||
|
this.flashTimer += deltaTime;
|
||||||
|
|
||||||
|
for (const entityId of entities) {
|
||||||
|
const invincibility = this.getComponent(entityId, Invincibility);
|
||||||
|
const meshComp = this.getComponent(entityId, MeshComponent);
|
||||||
|
|
||||||
|
if (!invincibility || !meshComp) continue;
|
||||||
|
|
||||||
|
// Update invincibility timer
|
||||||
|
invincibility.update(deltaTime);
|
||||||
|
|
||||||
|
// Flash effect - toggle visibility based on flash rate
|
||||||
|
if (invincibility.getIsInvincible()) {
|
||||||
|
const flashInterval = Math.floor(this.flashTimer / GameConfig.INVINCIBILITY_FLASH_RATE);
|
||||||
|
meshComp.mesh.visible = flashInterval % 2 === 0;
|
||||||
|
} else {
|
||||||
|
// Always visible when not invincible
|
||||||
|
meshComp.mesh.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset flash timer periodically to prevent overflow
|
||||||
|
if (this.flashTimer > 10) {
|
||||||
|
this.flashTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue