feat: implement Music and SoundEffects systems for enhanced audio management, including background music and sound effects playback
All checks were successful
Build and Publish Docker Image / Build and Validate (pull_request) Successful in 10s
All checks were successful
Build and Publish Docker Image / Build and Validate (pull_request) Successful in 10s
This commit is contained in:
parent
143072f0a0
commit
2213f64e60
16 changed files with 739 additions and 14 deletions
|
|
@ -33,7 +33,8 @@ export class AISystem extends System {
|
|||
const playerController = this.engine.systems.find(
|
||||
(s) => s.name === SystemName.PLAYER_CONTROLLER
|
||||
) as PlayerControllerSystem | undefined;
|
||||
const player = playerController ? playerController.getPlayerEntity() : null;
|
||||
const player = playerController?.getPlayerEntity();
|
||||
if (!player) return;
|
||||
const playerPos = player?.getComponent<Position>(ComponentType.POSITION);
|
||||
const config = GameConfig.AI;
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,8 @@ export class AbsorptionSystem extends System {
|
|||
vfxSystem.createAbsorption(entityPos.x, entityPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
this.engine.emit(Events.ABSORPTION, { entity });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
59
src/systems/MusicSystem.ts
Normal file
59
src/systems/MusicSystem.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { System } from '../core/System.ts';
|
||||
import { SystemName, ComponentType } from '../core/Constants.ts';
|
||||
import type { Entity } from '../core/Entity.ts';
|
||||
import type { Engine } from '../core/Engine.ts';
|
||||
import type { Music } from '../components/Music.ts';
|
||||
|
||||
/**
|
||||
* System responsible for managing background music playback.
|
||||
*/
|
||||
export class MusicSystem extends System {
|
||||
private audioContext: AudioContext | null;
|
||||
|
||||
constructor() {
|
||||
super(SystemName.MUSIC);
|
||||
this.requiredComponents = [ComponentType.MUSIC];
|
||||
this.priority = 5;
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the audio context when system is added to engine.
|
||||
*/
|
||||
init(engine: Engine): void {
|
||||
super.init(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process music entities - currently just ensures audio context exists.
|
||||
*/
|
||||
process(_deltaTime: number, entities: Entity[]): void {
|
||||
entities.forEach((entity) => {
|
||||
const music = entity.getComponent<Music>(ComponentType.MUSIC);
|
||||
if (!music) return;
|
||||
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the shared audio context.
|
||||
*/
|
||||
getAudioContext(): AudioContext {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext();
|
||||
}
|
||||
return this.audioContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume audio context (required after user interaction).
|
||||
*/
|
||||
resumeAudioContext(): void {
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,12 @@ export class ProjectileSystem extends System {
|
|||
if (targetHealthComp) {
|
||||
targetHealthComp.takeDamage(damage);
|
||||
|
||||
this.engine.emit(Events.PROJECTILE_IMPACT, {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
damage,
|
||||
});
|
||||
|
||||
const vfxSystem = this.engine.systems.find((s) => s.name === SystemName.VFX) as
|
||||
| VFXSystem
|
||||
| undefined;
|
||||
|
|
|
|||
|
|
@ -179,7 +179,6 @@ export class RenderSystem extends System {
|
|||
let frames = spriteData[sprite.animationState as string] || spriteData[AnimationState.IDLE];
|
||||
|
||||
if (!frames || !Array.isArray(frames)) {
|
||||
// Fallback to default slime animation if data structure is unexpected
|
||||
frames = SpriteLibrary[EntityType.SLIME][AnimationState.IDLE];
|
||||
}
|
||||
|
||||
|
|
@ -414,28 +413,24 @@ export class RenderSystem extends System {
|
|||
*/
|
||||
drawGlowEffect(sprite: Sprite): void {
|
||||
const ctx = this.ctx;
|
||||
const time = Date.now() * 0.001; // Time in seconds for pulsing
|
||||
const pulse = 0.5 + Math.sin(time * 3) * 0.3; // Pulsing between 0.2 and 0.8
|
||||
const time = Date.now() * 0.001;
|
||||
const pulse = 0.5 + Math.sin(time * 3) * 0.3;
|
||||
const baseRadius = Math.max(sprite.width, sprite.height) / 2;
|
||||
const glowRadius = baseRadius + 4 + pulse * 2;
|
||||
|
||||
// Create radial gradient for soft glow
|
||||
const gradient = ctx.createRadialGradient(0, 0, baseRadius, 0, 0, glowRadius);
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.4 * pulse})`);
|
||||
gradient.addColorStop(0.5, `rgba(0, 230, 255, ${0.3 * pulse})`);
|
||||
gradient.addColorStop(1, 'rgba(0, 230, 255, 0)');
|
||||
|
||||
// Draw multiple layers for a softer glow effect
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = 'screen';
|
||||
|
||||
// Outer glow layer
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, glowRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Inner bright core
|
||||
const innerGradient = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 0.6);
|
||||
innerGradient.addColorStop(0, `rgba(255, 255, 255, ${0.6 * pulse})`);
|
||||
innerGradient.addColorStop(1, 'rgba(0, 230, 255, 0)');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { System } from '../core/System.ts';
|
||||
import { SkillRegistry } from '../skills/SkillRegistry.ts';
|
||||
import { SystemName, ComponentType } from '../core/Constants.ts';
|
||||
import { Events } from '../core/EventBus.ts';
|
||||
import type { Entity } from '../core/Entity.ts';
|
||||
import type { Skills } from '../components/Skills.ts';
|
||||
import type { Intent } from '../components/Intent.ts';
|
||||
|
|
@ -55,6 +56,7 @@ export class SkillSystem extends System {
|
|||
if (skills) {
|
||||
skills.setCooldown(skillId, skill.cooldown);
|
||||
}
|
||||
this.engine.emit(Events.SKILL_COOLDOWN_STARTED, { skillId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
105
src/systems/SoundEffectsSystem.ts
Normal file
105
src/systems/SoundEffectsSystem.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import { System } from '../core/System.ts';
|
||||
import { SystemName, ComponentType } from '../core/Constants.ts';
|
||||
import { Events } from '../core/EventBus.ts';
|
||||
import type { Entity } from '../core/Entity.ts';
|
||||
import type { Engine } from '../core/Engine.ts';
|
||||
import type { SoundEffects } from '../components/SoundEffects.ts';
|
||||
import type { MusicSystem } from './MusicSystem.ts';
|
||||
|
||||
/**
|
||||
* System responsible for managing sound effects playback.
|
||||
* Follows ECS pattern: system processes entities with SoundEffects component.
|
||||
* Listens to game events and plays appropriate sound effects.
|
||||
*/
|
||||
export class SoundEffectsSystem extends System {
|
||||
private sfxEntity: Entity | null;
|
||||
|
||||
constructor() {
|
||||
super(SystemName.SOUND_EFFECTS);
|
||||
this.requiredComponents = [ComponentType.SOUND_EFFECTS];
|
||||
this.priority = 5;
|
||||
this.sfxEntity = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize event listeners when system is added to engine.
|
||||
*/
|
||||
init(engine: Engine): void {
|
||||
super.init(engine);
|
||||
|
||||
this.engine.on(Events.ATTACK_PERFORMED, () => {
|
||||
this.playSound('attack');
|
||||
});
|
||||
|
||||
this.engine.on(Events.DAMAGE_DEALT, () => {
|
||||
this.playSound('damage');
|
||||
});
|
||||
|
||||
this.engine.on(Events.ABSORPTION, () => {
|
||||
this.playSound('absorb');
|
||||
});
|
||||
|
||||
this.engine.on(Events.SKILL_LEARNED, () => {
|
||||
this.playSound('skill');
|
||||
});
|
||||
|
||||
this.engine.on(Events.SKILL_COOLDOWN_STARTED, () => {
|
||||
this.playSound('skill');
|
||||
});
|
||||
|
||||
this.engine.on(Events.PROJECTILE_CREATED, () => {
|
||||
this.playSound('shoot');
|
||||
});
|
||||
|
||||
this.engine.on(Events.PROJECTILE_IMPACT, () => {
|
||||
this.playSound('impact');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sound effect entities - ensures audio context is available.
|
||||
*/
|
||||
process(_deltaTime: number, entities: Entity[]): void {
|
||||
entities.forEach((entity) => {
|
||||
const sfx = entity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
|
||||
if (!sfx) return;
|
||||
|
||||
if (!this.sfxEntity) {
|
||||
this.sfxEntity = entity;
|
||||
}
|
||||
|
||||
if (!sfx.audioContext) {
|
||||
const musicSystem = this.engine.systems.find((s) => s.name === SystemName.MUSIC) as
|
||||
| MusicSystem
|
||||
| undefined;
|
||||
if (musicSystem) {
|
||||
sfx.audioContext = musicSystem.getAudioContext();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect from any entity with SoundEffects component.
|
||||
* @param soundName - The name of the sound to play
|
||||
*/
|
||||
playSound(soundName: string): void {
|
||||
if (this.sfxEntity) {
|
||||
const sfx = this.sfxEntity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
|
||||
if (sfx) {
|
||||
sfx.play(soundName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const entities = this.engine.getEntities();
|
||||
for (const entity of entities) {
|
||||
const sfx = entity.getComponent<SoundEffects>(ComponentType.SOUND_EFFECTS);
|
||||
if (sfx) {
|
||||
this.sfxEntity = entity;
|
||||
sfx.play(soundName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue