Pārlūkot izejas kodu

Connect unit selection events to audio and add integration test

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 1 mēnesi atpakaļ
vecāks
revīzija
d8cee4254e

+ 11 - 0
CMakeLists.txt

@@ -148,8 +148,10 @@ file(COPY assets/ DESTINATION ${CMAKE_BINARY_DIR}/assets/)
 # ---- Test executables ----
 if(QT_VERSION_MAJOR EQUAL 6)
     qt6_add_executable(test_audio_event_handler test_audio_event_handler.cpp)
+    qt6_add_executable(test_audio_integration test_audio_integration.cpp)
 else()
     add_executable(test_audio_event_handler test_audio_event_handler.cpp)
+    add_executable(test_audio_integration test_audio_integration.cpp)
 endif()
 
 target_link_libraries(test_audio_event_handler
@@ -160,6 +162,15 @@ target_link_libraries(test_audio_event_handler
         audio_system
 )
 
+target_link_libraries(test_audio_integration
+    PRIVATE
+        Qt${QT_VERSION_MAJOR}::Core
+        Qt${QT_VERSION_MAJOR}::Multimedia
+        engine_core
+        audio_system
+        game_systems
+)
+
 # ---- clang-format helpers (optional but convenient) ----
 # Provides:
 #   - clang-format        : formats all C/C++ sources using .clang-format

+ 3 - 3
app/core/game_engine.cpp

@@ -103,9 +103,9 @@ GameEngine::GameEngine() {
   m_plant = std::make_unique<Render::GL::PlantRenderer>();
   m_pine = std::make_unique<Render::GL::PineRenderer>();
 
-  m_passes = {m_ground.get(), m_terrain.get(), m_river.get(),
-              m_riverbank.get(), m_bridge.get(), m_biome.get(), m_stone.get(),
-              m_plant.get(),  m_pine.get(),    m_fog.get()};
+  m_passes = {m_ground.get(), m_terrain.get(), m_river.get(), m_riverbank.get(),
+              m_bridge.get(), m_biome.get(),   m_stone.get(), m_plant.get(),
+              m_pine.get(),   m_fog.get()};
 
   std::unique_ptr<Engine::Core::System> arrowSys =
       std::make_unique<Game::Systems::ArrowSystem>();

+ 101 - 73
assets/shaders/river.frag

@@ -7,127 +7,155 @@ uniform float time;
 
 // ------------------------------------------------------------
 const float PI = 3.14159265359;
-float saturate(float x){ return clamp(x, 0.0, 1.0); }
-vec3  saturate(vec3 v){ return clamp(v, vec3(0.0), vec3(1.0)); }
-mat2 rot(float a){ float c=cos(a), s=sin(a); return mat2(c,-s,s,c); }
-
-float hash(vec2 p){ p=fract(p*vec2(123.34,456.21)); p+=dot(p,p+45.32); return fract(p.x*p.y); }
-float noise(vec2 p){ vec2 i=floor(p), f=fract(p); f=f*f*(3.0-2.0*f);
-  float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1));
-  return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec3 saturate(vec3 v) { return clamp(v, vec3(0.0), vec3(1.0)); }
+mat2 rot(float a) {
+  float c = cos(a), s = sin(a);
+  return mat2(c, -s, s, c);
 }
-float fbm(vec2 p){ float v=0., a=.5, f=1.; for(int i=0;i<5;i++){ v+=a*noise(p*f); f*=2.; a*=.5; } return v; }
-
-vec3 skyColor(vec3 rd, vec3 sunDir){
-  float t = saturate(rd.y*0.5+0.5);
-  vec3 horizon=vec3(0.68,0.84,0.95), zenith=vec3(0.15,0.36,0.70);
-  vec3 sky=mix(horizon,zenith,t);
-  float sun = pow(max(dot(rd,sunDir),0.0), 260.0);
-  float halo= pow(max(dot(rd,sunDir),0.0), 6.0) * 0.03;
-  return sky + vec3(1.0,0.96,0.88) * (sun*1.0 + halo);
+
+float hash(vec2 p) {
+  p = fract(p * vec2(123.34, 456.21));
+  p += dot(p, p + 45.32);
+  return fract(p.x * p.y);
+}
+float noise(vec2 p) {
+  vec2 i = floor(p), f = fract(p);
+  f = f * f * (3.0 - 2.0 * f);
+  float a = hash(i), b = hash(i + vec2(1, 0)), c = hash(i + vec2(0, 1)),
+        d = hash(i + vec2(1, 1));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
+}
+float fbm(vec2 p) {
+  float v = 0., a = .5, f = 1.;
+  for (int i = 0; i < 5; i++) {
+    v += a * noise(p * f);
+    f *= 2.;
+    a *= .5;
+  }
+  return v;
+}
+
+vec3 skyColor(vec3 rd, vec3 sunDir) {
+  float t = saturate(rd.y * 0.5 + 0.5);
+  vec3 horizon = vec3(0.68, 0.84, 0.95), zenith = vec3(0.15, 0.36, 0.70);
+  vec3 sky = mix(horizon, zenith, t);
+  float sun = pow(max(dot(rd, sunDir), 0.0), 260.0);
+  float halo = pow(max(dot(rd, sunDir), 0.0), 6.0) * 0.03;
+  return sky + vec3(1.0, 0.96, 0.88) * (sun * 1.0 + halo);
+}
+float fresnelSchlick(float c, float F0) {
+  return F0 + (1.0 - F0) * pow(1.0 - c, 5.0);
 }
-float fresnelSchlick(float c, float F0){ return F0 + (1.0 - F0) * pow(1.0 - c, 5.0); }
-float ggxSpec(vec3 N, vec3 V, vec3 L, float rough, float F0){
-  vec3 H=normalize(V+L);
-  float NdotV=max(dot(N,V),0.0), NdotL=max(dot(N,L),0.0);
-  float NdotH=max(dot(N,H),0.0), VdotH=max(dot(V,H),0.0);
-  float a=max(rough*rough,0.001), a2=a*a;
-  float denom=(NdotH*NdotH*(a2-1.0)+1.0);
-  float D=a2/(PI*denom*denom);
-  float k=(a+1.0); k=(k*k)/8.0;
-  float Gv=NdotV/(NdotV*(1.0-k)+k), Gl=NdotL/(NdotL*(1.0-k)+k);
-  float F=fresnelSchlick(VdotH,F0);
-  return (D*Gv*Gl*F)/max(4.0*NdotV*NdotL,0.001);
+float ggxSpec(vec3 N, vec3 V, vec3 L, float rough, float F0) {
+  vec3 H = normalize(V + L);
+  float NdotV = max(dot(N, V), 0.0), NdotL = max(dot(N, L), 0.0);
+  float NdotH = max(dot(N, H), 0.0), VdotH = max(dot(V, H), 0.0);
+  float a = max(rough * rough, 0.001), a2 = a * a;
+  float denom = (NdotH * NdotH * (a2 - 1.0) + 1.0);
+  float D = a2 / (PI * denom * denom);
+  float k = (a + 1.0);
+  k = (k * k) / 8.0;
+  float Gv = NdotV / (NdotV * (1.0 - k) + k),
+        Gl = NdotL / (NdotL * (1.0 - k) + k);
+  float F = fresnelSchlick(VdotH, F0);
+  return (D * Gv * Gl * F) / max(4.0 * NdotV * NdotL, 0.001);
 }
 
 // ---------------- water field (isotropic, world-space) ---------------
-float waterHeight(vec2 uv){
+float waterHeight(vec2 uv) {
   vec2 p = uv;
-  vec2 w1 = vec2(fbm(p*0.6 + time*0.05), fbm(p*0.6 - time*0.04));
-  vec2 w2 = vec2(fbm(rot(1.3)*p*0.9 - time*0.03), fbm(rot(2.1)*p*0.7 + time*0.02));
-  p += 0.75*w1 + 0.45*w2;
+  vec2 w1 = vec2(fbm(p * 0.6 + time * 0.05), fbm(p * 0.6 - time * 0.04));
+  vec2 w2 = vec2(fbm(rot(1.3) * p * 0.9 - time * 0.03),
+                 fbm(rot(2.1) * p * 0.7 + time * 0.02));
+  p += 0.75 * w1 + 0.45 * w2;
   float h = 0.0;
-  h += 0.55 * (fbm(p*1.6 - time*0.15)          - 0.5);
-  h += 0.30 * (fbm(rot(0.8)*p*2.8 + time*0.20) - 0.5);
-  h += 0.15 * (fbm(rot(2.4)*p*5.0 - time*0.35) - 0.5);
+  h += 0.55 * (fbm(p * 1.6 - time * 0.15) - 0.5);
+  h += 0.30 * (fbm(rot(0.8) * p * 2.8 + time * 0.20) - 0.5);
+  h += 0.15 * (fbm(rot(2.4) * p * 5.0 - time * 0.35) - 0.5);
   return h;
 }
 
-void heightDeriv(vec2 uv, out float h, out vec2 grad, out float lap){
+void heightDeriv(vec2 uv, out float h, out vec2 grad, out float lap) {
   float s = max(0.003, 0.35 * length(fwidth(uv)));
-  vec2  e = vec2(s);
-  float hc  = waterHeight(uv);
-  float hx1 = waterHeight(uv + vec2(e.x,0));
-  float hx2 = waterHeight(uv - vec2(e.x,0));
-  float hy1 = waterHeight(uv + vec2(0,e.y));
-  float hy2 = waterHeight(uv - vec2(0,e.y));
-  grad = vec2((hx1-hx2)/(2.0*e.x), (hy1-hy2)/(2.0*e.y)) * 0.85;
-  lap  = (hx1+hx2+hy1+hy2-4.0*hc)/(e.x*e.x+e.y*e.y);
+  vec2 e = vec2(s);
+  float hc = waterHeight(uv);
+  float hx1 = waterHeight(uv + vec2(e.x, 0));
+  float hx2 = waterHeight(uv - vec2(e.x, 0));
+  float hy1 = waterHeight(uv + vec2(0, e.y));
+  float hy2 = waterHeight(uv - vec2(0, e.y));
+  grad = vec2((hx1 - hx2) / (2.0 * e.x), (hy1 - hy2) / (2.0 * e.y)) * 0.85;
+  lap = (hx1 + hx2 + hy1 + hy2 - 4.0 * hc) / (e.x * e.x + e.y * e.y);
   h = hc;
 }
 
-vec3 microNormal(vec2 uv){
-  float s=7.0;
+vec3 microNormal(vec2 uv) {
+  float s = 7.0;
   vec2 e = vec2(max(0.0015, 0.5 * length(fwidth(uv))));
-  float mx1=fbm(uv*s+time*0.6+vec2(e.x,0)), mx2=fbm(uv*s+time*0.6-vec2(e.x,0));
-  float my1=fbm(uv*s+time*0.6+vec2(0,e.y)), my2=fbm(uv*s+time*0.6-vec2(0,e.y));
-  vec2 g=vec2((mx1-mx2)/(2.0*e.x), (my1-my2)/(2.0*e.y));
+  float mx1 = fbm(uv * s + time * 0.6 + vec2(e.x, 0)),
+        mx2 = fbm(uv * s + time * 0.6 - vec2(e.x, 0));
+  float my1 = fbm(uv * s + time * 0.6 + vec2(0, e.y)),
+        my2 = fbm(uv * s + time * 0.6 - vec2(0, e.y));
+  vec2 g = vec2((mx1 - mx2) / (2.0 * e.x), (my1 - my2) / (2.0 * e.y));
   return normalize(vec3(-g.x, 0.35, -g.y));
 }
-vec3 waterNormal(vec2 uv, vec2 grad){
-  float k=3.2;
-  vec3 N = normalize(vec3(-grad.x*k, 1.0, -grad.y*k));
-  return normalize(mix(N, normalize(N + 0.22*(microNormal(uv)-vec3(0,1,0))), 0.35));
+vec3 waterNormal(vec2 uv, vec2 grad) {
+  float k = 3.2;
+  vec3 N = normalize(vec3(-grad.x * k, 1.0, -grad.y * k));
+  return normalize(
+      mix(N, normalize(N + 0.22 * (microNormal(uv) - vec3(0, 1, 0))), 0.35));
 }
 
 // ---------------- main ----------------
-void main(){
+void main() {
   // world-space UV so no seams
   vec2 uv = rot(0.35) * (WorldPos.xz * 0.38);
 
-  float h, lap; vec2 grad;
+  float h, lap;
+  vec2 grad;
   heightDeriv(uv, h, grad, lap);
   vec3 N = waterNormal(uv, grad);
 
   vec3 sunDir = normalize(vec3(0.28, 0.85, 0.43));
-  vec3 V      = normalize(vec3(0.0, 0.7, 0.7));
+  vec3 V = normalize(vec3(0.0, 0.7, 0.7));
 
-  float NdotV = max(dot(N,V), 0.0);
+  float NdotV = max(dot(N, V), 0.0);
   float F0 = 0.02;
 
   // ---------- transmission (make it BLUER) ----------
-  vec3 deepWater    = vec3(0.008, 0.035, 0.080);  // deeper, bluer
+  vec3 deepWater = vec3(0.008, 0.035, 0.080); // deeper, bluer
   vec3 shallowWater = vec3(0.060, 0.180, 0.300);
 
   float calm = smoothstep(0.0, 0.45, abs(h));
-  float shallow = saturate(0.35 + 0.35 * (fbm(uv*0.6) * (1.0 - calm)));
+  float shallow = saturate(0.35 + 0.35 * (fbm(uv * 0.6) * (1.0 - calm)));
 
   // stronger red/green absorption so blue survives
-  vec3  absorb    = vec3(0.90, 0.45, 0.12);
-  float thickness = mix(0.6, 3.5, 1.0 - shallow) * (0.35 + pow(1.0 - NdotV, 0.7));
-  vec3  transBase = mix(deepWater, shallowWater, shallow);
-  vec3  transmission = transBase * exp(-absorb * thickness);
+  vec3 absorb = vec3(0.90, 0.45, 0.12);
+  float thickness =
+      mix(0.6, 3.5, 1.0 - shallow) * (0.35 + pow(1.0 - NdotV, 0.7));
+  vec3 transBase = mix(deepWater, shallowWater, shallow);
+  vec3 transmission = transBase * exp(-absorb * thickness);
 
   // ---------- reflection (dim + blue-tinted) ----------
   vec3 R = reflect(-V, N);
   vec3 reflection = skyColor(R, sunDir);
-  reflection *= 0.70;                              // dim
-  reflection *= vec3(0.60, 0.75, 1.00);            // blue tint
-  float F = fresnelSchlick(NdotV, F0) * 0.40;      // less mirror
+  reflection *= 0.70;                         // dim
+  reflection *= vec3(0.60, 0.75, 1.00);       // blue tint
+  float F = fresnelSchlick(NdotV, F0) * 0.40; // less mirror
 
   // ---------- lighting (tamed) ----------
   float NdotL = max(dot(N, sunDir), 0.0);
   float rough = mix(0.12, 0.26, smoothstep(0.0, 0.6, length(grad)));
-  float spec  = ggxSpec(N, V, sunDir, rough, F0) * 0.50; // half energy
-  vec3  specCol = vec3(0.75, 0.85, 1.10) * spec;         // watery tint
-  vec3  sunDiffuse = transmission * NdotL * 0.20;
+  float spec = ggxSpec(N, V, sunDir, rough, F0) * 0.50; // half energy
+  vec3 specCol = vec3(0.75, 0.85, 1.10) * spec;         // watery tint
+  vec3 sunDiffuse = transmission * NdotL * 0.20;
 
   // shoreline foam only (softer & less white)
   float shore = 1.0 - (smoothstep(0.07, 0.28, TexCoord.y) *
                        smoothstep(0.07, 0.28, 1.0 - TexCoord.y));
-  float foam  = shore * (0.45 + 0.55 * fbm(uv*3.0 + time*0.6));
-  vec3  foamCol = vec3(0.92, 0.96, 1.0);
-  foam = clamp(foam * 0.35, 0.0, 1.0);             // much less foam
+  float foam = shore * (0.45 + 0.55 * fbm(uv * 3.0 + time * 0.6));
+  vec3 foamCol = vec3(0.92, 0.96, 1.0);
+  foam = clamp(foam * 0.35, 0.0, 1.0); // much less foam
 
   // ---------- combine (energy aware) ----------
   vec3 color = transmission * (1.0 - F) + reflection * F; // avoid whitening

+ 45 - 33
assets/shaders/riverbank.frag

@@ -1,37 +1,42 @@
 #version 330 core
 out vec4 FragColor;
 
-in vec2 TexCoord;   // x: 0 = water edge -> 1 = land side, y: along the river
+in vec2 TexCoord; // x: 0 = water edge -> 1 = land side, y: along the river
 in vec3 WorldPos;
 in vec3 Normal;
 
 uniform float time;
 
 // ------------------------- utils -------------------------
-float saturate(float x){ return clamp(x, 0.0, 1.0); }
-vec2  saturate(vec2 v){ return clamp(v, vec2(0.0), vec2(1.0)); }
-vec3  saturate(vec3 v){ return clamp(v, vec3(0.0), vec3(1.0)); }
+float saturate(float x) { return clamp(x, 0.0, 1.0); }
+vec2 saturate(vec2 v) { return clamp(v, vec2(0.0), vec2(1.0)); }
+vec3 saturate(vec3 v) { return clamp(v, vec3(0.0), vec3(1.0)); }
 
-float hash(vec2 p){
+float hash(vec2 p) {
   p = fract(p * vec2(123.34, 456.21));
   p += dot(p, p + 45.32);
   return fract(p.x * p.y);
 }
-float noise(vec2 p){
+float noise(vec2 p) {
   vec2 i = floor(p), f = fract(p);
-  f = f*f*(3.0-2.0*f);
-  float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1));
-  return mix(mix(a,b,f.x), mix(c,d,f.x), f.y);
+  f = f * f * (3.0 - 2.0 * f);
+  float a = hash(i), b = hash(i + vec2(1, 0)), c = hash(i + vec2(0, 1)),
+        d = hash(i + vec2(1, 1));
+  return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
 }
-float fbm(vec2 p){
-  float v=0.0, a=0.5, f=1.0;
-  for(int i=0;i<5;i++){ v += a*noise(p*f); f*=2.0; a*=0.5; }
+float fbm(vec2 p) {
+  float v = 0.0, a = 0.5, f = 1.0;
+  for (int i = 0; i < 5; i++) {
+    v += a * noise(p * f);
+    f *= 2.0;
+    a *= 0.5;
+  }
   return v;
 }
-vec2 warp(vec2 uv){
-  vec2 w1 = vec2(fbm(uv*0.7 + time*0.06), fbm(uv*0.7 - time*0.05));
-  vec2 w2 = vec2(fbm(uv*1.1 - time*0.03), fbm(uv*0.9 + time*0.04));
-  return uv + 0.22*w1 + 0.12*w2;
+vec2 warp(vec2 uv) {
+  vec2 w1 = vec2(fbm(uv * 0.7 + time * 0.06), fbm(uv * 0.7 - time * 0.05));
+  vec2 w2 = vec2(fbm(uv * 1.1 - time * 0.03), fbm(uv * 0.9 + time * 0.04));
+  return uv + 0.22 * w1 + 0.12 * w2;
 }
 
 // ------------------------- main -------------------------
@@ -40,44 +45,50 @@ void main() {
   vec2 uv = warp(WorldPos.xz * 0.45);
 
   // --- Palettes (slightly lighter and more natural) ---
-  vec3 wetSoil   = vec3(0.20, 0.17, 0.14);
-  vec3 dampSoil  = vec3(0.32, 0.27, 0.22);
-  vec3 drySoil   = vec3(0.46, 0.40, 0.33);
+  vec3 wetSoil = vec3(0.20, 0.17, 0.14);
+  vec3 dampSoil = vec3(0.32, 0.27, 0.22);
+  vec3 drySoil = vec3(0.46, 0.40, 0.33);
   vec3 grassTint = vec3(0.30, 0.50, 0.25);
 
   // --- Wetness computation ---
   // Base width from cross-river coord (narrower; was too thick)
-  float baseWet = smoothstep(0.30, 0.04, TexCoord.x); // 1 near water, 0 towards land
+  float baseWet =
+      smoothstep(0.30, 0.04, TexCoord.x); // 1 near water, 0 towards land
 
   // Irregular shoreline: jitter width with world-space FBM and along-flow noise
-  float edgeJitter = (fbm(uv * 2.5) - 0.5) * 0.12 + (fbm(vec2(TexCoord.y * 3.0, 0.0)) - 0.5) * 0.10;
+  float edgeJitter = (fbm(uv * 2.5) - 0.5) * 0.12 +
+                     (fbm(vec2(TexCoord.y * 3.0, 0.0)) - 0.5) * 0.10;
   baseWet = saturate(baseWet + edgeJitter);
 
   // Slope-aware: steeper banks drain faster (less wet)
   float slope = 1.0 - saturate(normalize(Normal).y);
   float wetness = saturate(baseWet - slope * 0.45);
 
-  // A super-narrow “water contact” band to soften the hard polygon edge visually
-  float contact = smoothstep(0.10, 0.95, wetness) * smoothstep(0.04, 0.00, TexCoord.x);
+  // A super-narrow “water contact” band to soften the hard polygon edge
+  // visually
+  float contact =
+      smoothstep(0.10, 0.95, wetness) * smoothstep(0.04, 0.00, TexCoord.x);
   contact *= 0.6 + 0.4 * fbm(uv * 4.0 + time * 0.2); // noisy, thin
 
   // --- Macro variation & flow-aligned streaks (erosion/silt) ---
-  float macro   = fbm(uv * 0.8);
-  float streaks = fbm(vec2(TexCoord.y * 6.0 + macro * 0.7, TexCoord.x * 0.6 - time * 0.03));
+  float macro = fbm(uv * 0.8);
+  float streaks =
+      fbm(vec2(TexCoord.y * 6.0 + macro * 0.7, TexCoord.x * 0.6 - time * 0.03));
   streaks = pow(saturate(streaks), 3.0); // thin dark streak lines
 
   // Small grit & sparse pebbles
-  float grit    = noise(uv * 18.0);
+  float grit = noise(uv * 18.0);
   float pebbles = smoothstep(0.82, 0.97, noise(uv * 9.0 + 17.3));
 
   // --- Albedo synthesis ---
   // Wet->dry soil ramp with macro tinting
   vec3 soilWet = mix(wetSoil, dampSoil, saturate(macro * 0.5 + 0.25));
-  vec3 soilDry = mix(dampSoil, drySoil,  saturate(macro * 0.5 + 0.25));
-  vec3 base    = mix(soilWet, soilDry, 1.0 - wetness);
+  vec3 soilDry = mix(dampSoil, drySoil, saturate(macro * 0.5 + 0.25));
+  vec3 base = mix(soilWet, soilDry, 1.0 - wetness);
 
   // Grass appears as it gets drier, patchy by macro noise
-  float grassMask = smoothstep(0.50, 0.92, 1.0 - wetness) * smoothstep(0.25, 0.75, fbm(uv * 1.2 + 5.1));
+  float grassMask = smoothstep(0.50, 0.92, 1.0 - wetness) *
+                    smoothstep(0.25, 0.75, fbm(uv * 1.2 + 5.1));
   base = mix(base, mix(base, grassTint, 0.6), grassMask);
 
   // Soft cool tint right at the contact band (reduces “brown outline”)
@@ -85,7 +96,8 @@ void main() {
 
   // Grit/pebble breakup (subtle, only really reads when dry)
   base *= (0.92 + 0.16 * grit);
-  base  = mix(base, base * 0.82 + vec3(0.05), pebbles * (0.10 + 0.35 * (1.0 - wetness)));
+  base = mix(base, base * 0.82 + vec3(0.05),
+             pebbles * (0.10 + 0.35 * (1.0 - wetness)));
 
   // --- Lighting ---
   vec3 L = normalize(vec3(0.3, 0.8, 0.4));
@@ -98,13 +110,13 @@ void main() {
   float darken = mix(1.0, 0.94, wetness);
 
   vec3 ambient = vec3(0.42, 0.44, 0.46);
-  vec3 color   = base * darken * (ambient + NdotL * vec3(0.66, 0.64, 0.60));
+  vec3 color = base * darken * (ambient + NdotL * vec3(0.66, 0.64, 0.60));
 
   // Wet specular: only very near the water edge; cool tint
-  vec3  H = normalize(L + V);
+  vec3 H = normalize(L + V);
   float shininess = mix(10.0, 38.0, saturate(wetness * 1.2));
   float spec = pow(max(dot(N, H), 0.0), shininess);
-  spec *= (0.10 + 0.50 * contact);  // confined to the thin contact zone
+  spec *= (0.10 + 0.50 * contact); // confined to the thin contact zone
   color += spec * vec3(0.30, 0.33, 0.36);
 
   // Flow-aligned silt darkening (very gentle)

+ 3 - 3
assets/shaders/riverbank.vert

@@ -14,14 +14,14 @@ out vec3 Normal;
 
 void main() {
   vec3 pos = aPos;
-  
+
   // Subtle movement for organic feel
   float sway = sin(aPos.x * 0.3 + time * 0.5) * 0.005;
   pos.y += sway;
-  
+
   WorldPos = (model * vec4(pos, 1.0)).xyz;
   Normal = normalize(mat3(transpose(inverse(model))) * aNormal);
-  
+
   gl_Position = projection * view * model * vec4(pos, 1.0);
   TexCoord = aTexCoord;
 }

+ 6 - 6
game/audio/AudioEventHandler.cpp

@@ -1,8 +1,8 @@
 #include "AudioEventHandler.h"
-#include "AudioSystem.h"
 #include "../core/component.h"
 #include "../core/entity.h"
 #include "../core/world.h"
+#include "AudioSystem.h"
 
 namespace Game {
 namespace Audio {
@@ -17,11 +17,11 @@ bool AudioEventHandler::initialize() {
     return true;
   }
 
-  m_unitSelectedSub = Engine::Core::ScopedEventSubscription<
-      Engine::Core::UnitSelectedEvent>(
-      [this](const Engine::Core::UnitSelectedEvent &event) {
-        onUnitSelected(event);
-      });
+  m_unitSelectedSub =
+      Engine::Core::ScopedEventSubscription<Engine::Core::UnitSelectedEvent>(
+          [this](const Engine::Core::UnitSelectedEvent &event) {
+            onUnitSelected(event);
+          });
 
   m_ambientChangedSub = Engine::Core::ScopedEventSubscription<
       Engine::Core::AmbientStateChangedEvent>(

+ 2 - 2
game/audio/AudioEventHandler.h

@@ -29,8 +29,8 @@ public:
 
 private:
   void onUnitSelected(const Engine::Core::UnitSelectedEvent &event);
-  void onAmbientStateChanged(
-      const Engine::Core::AmbientStateChangedEvent &event);
+  void
+  onAmbientStateChanged(const Engine::Core::AmbientStateChangedEvent &event);
   void onAudioTrigger(const Engine::Core::AudioTriggerEvent &event);
   void onMusicTrigger(const Engine::Core::MusicTriggerEvent &event);
 

+ 114 - 14
game/audio/README.md

@@ -1,6 +1,6 @@
 # Audio System
 
-The audio system provides thread-safe audio playback for sounds and music using QtMultimedia.
+The audio system provides thread-safe audio playback for sounds and music using QtMultimedia, with full integration into the game's event system.
 
 ## Features
 
@@ -10,6 +10,15 @@ The audio system provides thread-safe audio playback for sounds and music using
 - **Music Streams**: Background music with crossfade capability
 - **Volume Controls**: Master, sound, and music volume controls
 - **Resource Management**: Load and manage multiple audio files
+- **Event Integration**: Automatic audio responses to game events
+
+## Components
+
+### AudioSystem
+Main audio playback system that manages sound effects and music tracks.
+
+### AudioEventHandler  
+Connects game events to audio responses, enabling automatic sound/music playback based on game state.
 
 ## API Overview
 
@@ -17,29 +26,54 @@ The audio system provides thread-safe audio playback for sounds and music using
 
 ```cpp
 #include "game/audio/AudioSystem.h"
+#include "game/audio/AudioEventHandler.h"
 
 auto& audioSystem = AudioSystem::getInstance();
 audioSystem.initialize();
+
+// Create handler with world reference
+Engine::Core::World world;
+Game::Audio::AudioEventHandler handler(&world);
+handler.initialize();
 ```
 
 ### Loading Audio
 
 ```cpp
-// Load a sound effect
+// Load sound effects
+audioSystem.loadSound("archer_voice", "assets/audio/voices/archer_voice.wav");
 audioSystem.loadSound("explosion", "assets/sounds/explosion.wav");
 
 // Load background music
-audioSystem.loadMusic("theme", "assets/music/theme.mp3");
+audioSystem.loadMusic("peaceful", "assets/audio/music/peaceful.wav");
+audioSystem.loadMusic("combat", "assets/audio/music/combat.wav");
+```
+
+### Configuring Event Mappings
+
+```cpp
+// Map unit types to voice lines
+handler.loadUnitVoiceMapping("archer", "archer_voice");
+handler.loadUnitVoiceMapping("knight", "knight_voice");
+
+// Map ambient states to music
+handler.loadAmbientMusic(Engine::Core::AmbientState::PEACEFUL, "peaceful");
+handler.loadAmbientMusic(Engine::Core::AmbientState::COMBAT, "combat");
 ```
 
 ### Playing Audio
 
 ```cpp
-// Play a sound effect
+// Direct playback
 audioSystem.playSound("explosion", 1.0f, false, 10);
-
-// Play music with looping
 audioSystem.playMusic("theme", 0.8f, true);
+
+// Event-triggered playback
+Engine::Core::EventManager::instance().publish(
+    Engine::Core::AudioTriggerEvent("explosion", 0.8f));
+
+Engine::Core::EventManager::instance().publish(
+    Engine::Core::MusicTriggerEvent("combat", 0.6f));
 ```
 
 ### Volume Control
@@ -79,9 +113,57 @@ audioSystem.resumeAll();
 
 ```cpp
 // Clean shutdown
+handler.shutdown();
 audioSystem.shutdown();
 ```
 
+## Event Integration
+
+The audio system responds to the following events:
+
+### UnitSelectedEvent
+When a unit is selected, plays the corresponding voice line based on unit type.
+- **Published by**: `SelectionSystem::selectUnit()`
+- **Handler**: `AudioEventHandler::onUnitSelected()`
+
+### AmbientStateChangedEvent  
+When the ambient state changes (e.g., from peaceful to combat), switches background music.
+- **Published by**: Game logic when combat state changes
+- **Handler**: `AudioEventHandler::onAmbientStateChanged()`
+- **States**: PEACEFUL, TENSE, COMBAT, VICTORY, DEFEAT
+
+### AudioTriggerEvent
+Direct audio playback trigger with full control over parameters.
+- **Published by**: Any game system
+- **Handler**: `AudioEventHandler::onAudioTrigger()`
+- **Parameters**: soundId, volume, loop, priority
+
+### MusicTriggerEvent
+Direct music playback trigger with crossfade support.
+- **Published by**: Any game system  
+- **Handler**: `AudioEventHandler::onMusicTrigger()`
+- **Parameters**: musicId, volume, crossfade
+
+## Audio Resources
+
+### Directory Structure
+```
+assets/audio/
+├── voices/          # Unit voice lines
+│   ├── archer_voice.wav
+│   ├── knight_voice.wav
+│   └── spearman_voice.wav
+└── music/           # Background music
+    ├── peaceful.wav
+    ├── tense.wav
+    ├── combat.wav
+    ├── victory.wav
+    └── defeat.wav
+```
+
+### Placeholder Files
+Current audio files are simple sine wave tones for testing. Replace with actual game audio.
+
 ## Implementation Details
 
 ### AudioSystem
@@ -90,6 +172,12 @@ audioSystem.shutdown();
 - Resource management for sounds and music
 - Volume controls with proper mixing
 
+### AudioEventHandler
+- Subscribes to game events via EventManager
+- Maps unit types to voice line sounds
+- Maps ambient states to background music
+- Uses ScopedEventSubscription for automatic cleanup
+
 ### Sound
 - Uses `QSoundEffect` for low-latency sound effects
 - Supports looping and volume control
@@ -102,23 +190,35 @@ audioSystem.shutdown();
 
 ## Testing
 
-A standalone test application is provided in `test_audio.cpp`:
+Two test executables are provided:
 
+### test_audio_event_handler
+Basic test of the audio event handler functionality:
 ```bash
 cd build
-make audio_system
-./test_audio
+./test_audio_event_handler
+```
+
+### test_audio_integration
+Full integration test with unit creation and event publishing:
+```bash
+cd build
+./test_audio_integration
 ```
 
 ## Dependencies
 
 - Qt5/Qt6 Multimedia module
 - C++20 standard library (thread, atomic, mutex)
+- Game event system (EventManager)
 
 ## Future Enhancements
 
-- Audio pooling for frequently used sounds
-- 3D positional audio support
-- Audio mixing and effects
-- Compression and streaming optimization
-- Integration with game event system
+- [ ] 3D positional audio
+- [ ] Audio occlusion
+- [ ] Dynamic music layers based on intensity
+- [ ] Voice line queuing to prevent overlaps
+- [ ] Audio resource hot-reloading
+- [ ] Audio pooling for frequently used sounds
+- [ ] Compression and streaming optimization
+

+ 3 - 0
game/systems/selection_system.cpp

@@ -2,6 +2,7 @@
 #include "../../app/utils/selection_utils.h"
 #include "../../render/gl/camera.h"
 #include "../core/component.h"
+#include "../core/event_manager.h"
 #include "../core/world.h"
 #include "../game_config.h"
 #include "command_service.h"
@@ -19,6 +20,8 @@ void SelectionSystem::selectUnit(Engine::Core::EntityID unitId) {
   auto it = std::find(m_selectedUnits.begin(), m_selectedUnits.end(), unitId);
   if (it == m_selectedUnits.end()) {
     m_selectedUnits.push_back(unitId);
+    Engine::Core::EventManager::instance().publish(
+        Engine::Core::UnitSelectedEvent(unitId));
   }
 }
 

+ 2 - 1
render/gl/backend.cpp

@@ -1285,7 +1285,8 @@ void Backend::cacheRiverbankUniforms() {
 
   m_riverbankUniforms.model = m_riverbankShader->uniformHandle("model");
   m_riverbankUniforms.view = m_riverbankShader->uniformHandle("view");
-  m_riverbankUniforms.projection = m_riverbankShader->uniformHandle("projection");
+  m_riverbankUniforms.projection =
+      m_riverbankShader->uniformHandle("projection");
   m_riverbankUniforms.time = m_riverbankShader->uniformHandle("time");
 }
 

+ 4 - 2
render/gl/shader_cache.h

@@ -95,8 +95,10 @@ public:
     const QString riverFrag = kShaderBase + QStringLiteral("river.frag");
     load(QStringLiteral("river"), riverVert, riverFrag);
 
-    const QString riverbankVert = kShaderBase + QStringLiteral("riverbank.vert");
-    const QString riverbankFrag = kShaderBase + QStringLiteral("riverbank.frag");
+    const QString riverbankVert =
+        kShaderBase + QStringLiteral("riverbank.vert");
+    const QString riverbankFrag =
+        kShaderBase + QStringLiteral("riverbank.frag");
     load(QStringLiteral("riverbank"), riverbankVert, riverbankFrag);
 
     const QString bridgeVert = kShaderBase + QStringLiteral("bridge.vert");

+ 3 - 5
render/ground/biome_renderer.cpp

@@ -222,7 +222,6 @@ void BiomeRenderer::generateGrassInstances() {
     if (m_terrainTypes[normalIdx] == Game::Map::TerrainType::River)
       return false;
 
-    // Check for riverbank proximity - allow sparse grass instead of complete exclusion
     constexpr int kRiverMargin = 1;
     int nearRiverCount = 0;
     for (int dz = -kRiverMargin; dz <= kRiverMargin; ++dz) {
@@ -238,11 +237,10 @@ void BiomeRenderer::generateGrassInstances() {
         }
       }
     }
-    
-    // If near river, reduce grass density based on proximity
+
     if (nearRiverCount > 0) {
-      // Use random sampling to thin out grass near water
-      float riverbankDensity = 0.15f; // Only 15% of normal density near water
+
+      float riverbankDensity = 0.15f;
       if (rand01(state) > riverbankDensity)
         return false;
     }

+ 1 - 1
render/ground/riverbank_asset_gpu.h

@@ -10,7 +10,7 @@ struct RiverbankAssetInstanceGpu {
   std::array<float, 3> scale;
   std::array<float, 4> rotation;
   std::array<float, 3> color;
-  float assetType; // 0=pebble, 1=small rock, 2=reed cluster
+  float assetType;
 };
 
 struct RiverbankAssetBatchParams {

+ 12 - 30
render/ground/riverbank_asset_renderer.cpp

@@ -97,7 +97,6 @@ void RiverbankAssetRenderer::submit(Renderer &renderer,
   auto &visibility = Game::Map::VisibilityService::instance();
   const bool useVisibility = visibility.isInitialized();
 
-  // Filter instances by visibility
   std::vector<RiverbankAssetInstanceGpu> visibleInstances;
 
   for (const auto &instance : m_assetInstances) {
@@ -109,16 +108,15 @@ void RiverbankAssetRenderer::submit(Renderer &renderer,
       float worldZ = instance.position[2];
 
       if (visibility.isVisibleWorld(worldX, worldZ)) {
-        // Fully visible
+
         alpha = 1.0f;
       } else if (visibility.isExploredWorld(worldX, worldZ)) {
-        // Explored but not currently visible - darken
+
         alpha = 0.5f;
-        // For explored assets, we could either skip them or render darkened
-        // Skipping for consistency with bridges/riverbanks
+
         shouldRender = false;
       } else {
-        // Unseen - don't render
+
         shouldRender = false;
       }
     }
@@ -128,12 +126,8 @@ void RiverbankAssetRenderer::submit(Renderer &renderer,
     }
   }
 
-  // For now, we'll render these using the stone batch renderer
-  // In a full implementation, you'd create a custom shader for riverbank assets
-  // This is a simplified version that reuses existing rendering infrastructure
   if (!visibleInstances.empty()) {
-    // Note: This would require extending the renderer to support riverbank
-    // assets For minimal changes, we'll just prepare the data
+
     qDebug() << "RiverbankAssetRenderer: Would render"
              << visibleInstances.size() << "of" << m_assetInstanceCount
              << "riverbank assets (fog of war applied)";
@@ -177,7 +171,6 @@ void RiverbankAssetRenderer::generateAssetInstances() {
     return h0 * (1.0f - tz) + h1 * tz;
   };
 
-  // Generate assets along each river segment
   for (size_t segIdx = 0; segIdx < m_riverSegments.size(); ++segIdx) {
     const auto &segment = m_riverSegments[segIdx];
 
@@ -189,9 +182,8 @@ void RiverbankAssetRenderer::generateAssetInstances() {
     dir.normalize();
     QVector3D perpendicular(-dir.z(), 0.0f, dir.x());
     float halfRiverWidth = segment.width * 0.5f;
-    float bankZoneWidth = 1.5f; // Zone where we place assets
+    float bankZoneWidth = 1.5f;
 
-    // Number of potential asset positions along the river
     int numSteps = static_cast<int>(length / 0.8f) + 1;
 
     uint32_t rng = m_noiseSeed + static_cast<uint32_t>(segIdx * 1000);
@@ -201,15 +193,12 @@ void RiverbankAssetRenderer::generateAssetInstances() {
           static_cast<float>(i) / static_cast<float>(std::max(numSteps - 1, 1));
       QVector3D centerPos = segment.start + dir * (length * t);
 
-      // Random placement on either side of river
       for (int side = 0; side < 2; ++side) {
         float sideSign = (side == 0) ? -1.0f : 1.0f;
 
-        // Randomly decide if we place an asset here
         if (rand01(rng) > 0.3f)
-          continue; // 30% chance to place asset
+          continue;
 
-        // Random position within bank zone
         float distFromWater = halfRiverWidth + rand01(rng) * bankZoneWidth;
         float alongRiver = (rand01(rng) - 0.5f) * 0.6f;
 
@@ -217,7 +206,6 @@ void RiverbankAssetRenderer::generateAssetInstances() {
                              perpendicular * (sideSign * distFromWater) +
                              dir * alongRiver;
 
-        // Convert to grid coordinates
         float gx = (assetPos.x() / m_tileSize) + halfWidth;
         float gz = (assetPos.z() / m_tileSize) + halfHeight;
 
@@ -228,7 +216,6 @@ void RiverbankAssetRenderer::generateAssetInstances() {
         int iz = static_cast<int>(gz);
         int idx = iz * m_width + ix;
 
-        // Don't place on rivers, mountains, or hills
         if (m_terrainTypes[idx] != Game::Map::TerrainType::Flat)
           continue;
 
@@ -239,38 +226,35 @@ void RiverbankAssetRenderer::generateAssetInstances() {
         instance.position[1] = worldY;
         instance.position[2] = assetPos.z();
 
-        // Random asset type
         float typeRand = rand01(rng);
         if (typeRand < 0.7f) {
-          // Pebble (most common)
+
           instance.assetType = 0.0f;
           float size = 0.05f + rand01(rng) * 0.1f;
           instance.scale[0] = size * (0.8f + rand01(rng) * 0.4f);
           instance.scale[1] = size * (0.6f + rand01(rng) * 0.3f);
           instance.scale[2] = size * (0.8f + rand01(rng) * 0.4f);
 
-          // Gray/brown pebble colors
           float colorVar = 0.3f + rand01(rng) * 0.4f;
           instance.color[0] = colorVar;
           instance.color[1] = colorVar * 0.9f;
           instance.color[2] = colorVar * 0.85f;
         } else if (typeRand < 0.9f) {
-          // Small rock
+
           instance.assetType = 1.0f;
           float size = 0.1f + rand01(rng) * 0.15f;
           instance.scale[0] = size;
           instance.scale[1] = size * (0.7f + rand01(rng) * 0.4f);
           instance.scale[2] = size;
 
-          // Rock colors
           float colorVar = 0.35f + rand01(rng) * 0.25f;
           instance.color[0] = colorVar;
           instance.color[1] = colorVar * 0.95f;
           instance.color[2] = colorVar * 0.9f;
         } else {
-          // Reed cluster (near water)
+
           if (distFromWater > halfRiverWidth + 0.5f)
-            continue; // Only near water edge
+            continue;
 
           instance.assetType = 2.0f;
           float size = 0.3f + rand01(rng) * 0.4f;
@@ -278,14 +262,12 @@ void RiverbankAssetRenderer::generateAssetInstances() {
           instance.scale[1] = size;
           instance.scale[2] = size * 0.3f;
 
-          // Green/brown reed colors
           instance.color[0] = 0.25f + rand01(rng) * 0.15f;
           instance.color[1] = 0.35f + rand01(rng) * 0.25f;
           instance.color[2] = 0.15f + rand01(rng) * 0.1f;
         }
 
-        // Random rotation
-        float angle = rand01(rng) * 6.28318f; // 2*PI
+        float angle = rand01(rng) * 6.28318f;
         instance.rotation[0] = 0.0f;
         instance.rotation[1] = std::sin(angle * 0.5f);
         instance.rotation[2] = 0.0f;

+ 17 - 29
render/ground/riverbank_renderer.cpp

@@ -53,7 +53,6 @@ void RiverbankRenderer::buildMeshes() {
            c * (1.0f - fx) * fy + d * fx * fy;
   };
 
-  // Helper to sample terrain height at world position
   auto sampleHeight = [&](float worldX, float worldZ) -> float {
     if (m_heights.empty() || m_gridWidth == 0 || m_gridHeight == 0)
       return 0.0f;
@@ -84,7 +83,6 @@ void RiverbankRenderer::buildMeshes() {
     return h0 * (1.0f - tz) + h1 * tz;
   };
 
-  // Create riverbank transition zones on both sides of each river segment
   for (const auto &segment : m_riverSegments) {
     QVector3D dir = segment.end - segment.start;
     float length = dir.length();
@@ -97,7 +95,6 @@ void RiverbankRenderer::buildMeshes() {
     QVector3D perpendicular(-dir.z(), 0.0f, dir.x());
     float halfWidth = segment.width * 0.5f;
 
-    // Riverbank transition zone width (extends beyond water edge)
     float bankWidth = 0.2f;
 
     int lengthSteps =
@@ -111,7 +108,6 @@ void RiverbankRenderer::buildMeshes() {
       float t = static_cast<float>(i) / static_cast<float>(lengthSteps - 1);
       QVector3D centerPos = segment.start + dir * (length * t);
 
-      // Edge variation using noise
       float noiseFreq1 = 2.0f;
       float noiseFreq2 = 5.0f;
       float noiseFreq3 = 10.0f;
@@ -129,19 +125,15 @@ void RiverbankRenderer::buildMeshes() {
 
       float widthVariation = combinedNoise * halfWidth * 0.35f;
 
-      // Meander for natural curves
       float meander = noise(t * 3.0f, length * 0.1f) * 0.3f;
       QVector3D centerOffset = perpendicular * meander;
       centerPos += centerOffset;
 
-      // Create riverbank zones on both sides
-      // Inner edge (water edge)
       QVector3D innerLeft =
           centerPos - perpendicular * (halfWidth + widthVariation);
       QVector3D innerRight =
           centerPos + perpendicular * (halfWidth + widthVariation);
 
-      // Outer edge (land edge) - with additional variation
       float outerVariation =
           noise(centerPos.x() * 8.0f, centerPos.z() * 8.0f) * 0.5f;
       QVector3D outerLeft =
@@ -151,18 +143,17 @@ void RiverbankRenderer::buildMeshes() {
 
       float normal[3] = {0.0f, 1.0f, 0.0f};
 
-      // Left bank strip (4 vertices per segment)
       Vertex leftInner, leftOuter;
       float heightInnerLeft = sampleHeight(innerLeft.x(), innerLeft.z());
       float heightOuterLeft = sampleHeight(outerLeft.x(), outerLeft.z());
 
       leftInner.position[0] = innerLeft.x();
-      leftInner.position[1] = heightInnerLeft + 0.05f; // Slightly above terrain
+      leftInner.position[1] = heightInnerLeft + 0.05f;
       leftInner.position[2] = innerLeft.z();
       leftInner.normal[0] = normal[0];
       leftInner.normal[1] = normal[1];
       leftInner.normal[2] = normal[2];
-      leftInner.texCoord[0] = 0.0f; // Inner = wet
+      leftInner.texCoord[0] = 0.0f;
       leftInner.texCoord[1] = t;
       vertices.push_back(leftInner);
 
@@ -172,11 +163,10 @@ void RiverbankRenderer::buildMeshes() {
       leftOuter.normal[0] = normal[0];
       leftOuter.normal[1] = normal[1];
       leftOuter.normal[2] = normal[2];
-      leftOuter.texCoord[0] = 1.0f; // Outer = dry
+      leftOuter.texCoord[0] = 1.0f;
       leftOuter.texCoord[1] = t;
       vertices.push_back(leftOuter);
 
-      // Right bank strip
       Vertex rightInner, rightOuter;
       float heightInnerRight = sampleHeight(innerRight.x(), innerRight.z());
       float heightOuterRight = sampleHeight(outerRight.x(), outerRight.z());
@@ -187,7 +177,7 @@ void RiverbankRenderer::buildMeshes() {
       rightInner.normal[0] = normal[0];
       rightInner.normal[1] = normal[1];
       rightInner.normal[2] = normal[2];
-      rightInner.texCoord[0] = 0.0f; // Inner = wet
+      rightInner.texCoord[0] = 0.0f;
       rightInner.texCoord[1] = t;
       vertices.push_back(rightInner);
 
@@ -197,30 +187,28 @@ void RiverbankRenderer::buildMeshes() {
       rightOuter.normal[0] = normal[0];
       rightOuter.normal[1] = normal[1];
       rightOuter.normal[2] = normal[2];
-      rightOuter.texCoord[0] = 1.0f; // Outer = dry
+      rightOuter.texCoord[0] = 1.0f;
       rightOuter.texCoord[1] = t;
       vertices.push_back(rightOuter);
 
       if (i < lengthSteps - 1) {
         unsigned int idx0 = i * 4;
 
-        // Left bank triangles
-        indices.push_back(idx0 + 0); // left inner
-        indices.push_back(idx0 + 4); // next left inner
-        indices.push_back(idx0 + 1); // left outer
+        indices.push_back(idx0 + 0);
+        indices.push_back(idx0 + 4);
+        indices.push_back(idx0 + 1);
 
-        indices.push_back(idx0 + 1); // left outer
-        indices.push_back(idx0 + 4); // next left inner
-        indices.push_back(idx0 + 5); // next left outer
+        indices.push_back(idx0 + 1);
+        indices.push_back(idx0 + 4);
+        indices.push_back(idx0 + 5);
 
-        // Right bank triangles
-        indices.push_back(idx0 + 2); // right inner
-        indices.push_back(idx0 + 3); // right outer
-        indices.push_back(idx0 + 6); // next right inner
+        indices.push_back(idx0 + 2);
+        indices.push_back(idx0 + 3);
+        indices.push_back(idx0 + 6);
 
-        indices.push_back(idx0 + 3); // right outer
-        indices.push_back(idx0 + 7); // next right outer
-        indices.push_back(idx0 + 6); // next right inner
+        indices.push_back(idx0 + 3);
+        indices.push_back(idx0 + 7);
+        indices.push_back(idx0 + 6);
       }
     }
 

+ 2 - 4
test_audio_event_handler.cpp

@@ -32,10 +32,8 @@ int main(int argc, char *argv[]) {
   std::cout << "   ✓ Audio Event Handler initialized" << std::endl;
 
   std::cout << "\n4. Loading placeholder audio resources..." << std::endl;
-  audioSystem.loadSound("archer_voice",
-                        "assets/audio/voices/archer_voice.wav");
-  audioSystem.loadSound("knight_voice",
-                        "assets/audio/voices/knight_voice.wav");
+  audioSystem.loadSound("archer_voice", "assets/audio/voices/archer_voice.wav");
+  audioSystem.loadSound("knight_voice", "assets/audio/voices/knight_voice.wav");
   audioSystem.loadSound("spearman_voice",
                         "assets/audio/voices/spearman_voice.wav");
   std::cout << "   ✓ Loaded unit voice sounds" << std::endl;

+ 127 - 0
test_audio_integration.cpp

@@ -0,0 +1,127 @@
+#include "game/audio/AudioEventHandler.h"
+#include "game/audio/AudioSystem.h"
+#include "game/core/component.h"
+#include "game/core/event_manager.h"
+#include "game/core/world.h"
+#include "game/systems/selection_system.h"
+#include <QCoreApplication>
+#include <QTimer>
+#include <iostream>
+
+int main(int argc, char *argv[]) {
+  QCoreApplication app(argc, argv);
+
+  std::cout << "=== Audio Event Integration Test ===" << std::endl;
+
+  std::cout << "\n1. Initializing Audio System..." << std::endl;
+  auto &audioSystem = AudioSystem::getInstance();
+  if (!audioSystem.initialize()) {
+    std::cerr << "Failed to initialize audio system!" << std::endl;
+    return 1;
+  }
+  std::cout << "   ✓ Audio System initialized" << std::endl;
+
+  std::cout << "\n2. Creating World and Systems..." << std::endl;
+  Engine::Core::World world;
+  Game::Systems::SelectionSystem selectionSystem;
+  std::cout << "   ✓ World and SelectionSystem created" << std::endl;
+
+  std::cout << "\n3. Initializing Audio Event Handler..." << std::endl;
+  Game::Audio::AudioEventHandler handler(&world);
+  if (!handler.initialize()) {
+    std::cerr << "Failed to initialize audio event handler!" << std::endl;
+    return 1;
+  }
+  std::cout << "   ✓ Audio Event Handler initialized" << std::endl;
+
+  std::cout << "\n4. Loading placeholder audio resources..." << std::endl;
+  audioSystem.loadSound("archer_voice", "assets/audio/voices/archer_voice.wav");
+  audioSystem.loadSound("knight_voice", "assets/audio/voices/knight_voice.wav");
+  audioSystem.loadSound("spearman_voice",
+                        "assets/audio/voices/spearman_voice.wav");
+  audioSystem.loadMusic("peaceful", "assets/audio/music/peaceful.wav");
+  audioSystem.loadMusic("combat", "assets/audio/music/combat.wav");
+  std::cout << "   ✓ Audio resources loaded" << std::endl;
+
+  std::cout << "\n5. Configuring unit type mappings..." << std::endl;
+  handler.loadUnitVoiceMapping("archer", "archer_voice");
+  handler.loadUnitVoiceMapping("knight", "knight_voice");
+  handler.loadUnitVoiceMapping("spearman", "spearman_voice");
+  handler.loadAmbientMusic(Engine::Core::AmbientState::PEACEFUL, "peaceful");
+  handler.loadAmbientMusic(Engine::Core::AmbientState::COMBAT, "combat");
+  std::cout << "   ✓ Mappings configured" << std::endl;
+
+  std::cout << "\n6. Creating test units..." << std::endl;
+  auto *archerEntity = world.createEntity();
+  auto *archerUnit = archerEntity->addComponent<Engine::Core::UnitComponent>();
+  archerUnit->unitType = "archer";
+  archerUnit->health = 100;
+  archerUnit->maxHealth = 100;
+
+  auto *knightEntity = world.createEntity();
+  auto *knightUnit = knightEntity->addComponent<Engine::Core::UnitComponent>();
+  knightUnit->unitType = "knight";
+  knightUnit->health = 150;
+  knightUnit->maxHealth = 150;
+  std::cout << "   ✓ Created archer (ID: " << archerEntity->getId()
+            << ") and knight (ID: " << knightEntity->getId() << ")"
+            << std::endl;
+
+  std::cout << "\n7. Testing unit selection with voice playback..."
+            << std::endl;
+  std::cout << "   - Selecting archer..." << std::endl;
+  selectionSystem.selectUnit(archerEntity->getId());
+  std::cout
+      << "   ✓ Archer selected (should trigger archer_voice sound playback)"
+      << std::endl;
+
+  std::cout << "   - Selecting knight..." << std::endl;
+  selectionSystem.selectUnit(knightEntity->getId());
+  std::cout
+      << "   ✓ Knight selected (should trigger knight_voice sound playback)"
+      << std::endl;
+
+  std::cout << "\n8. Testing ambient state changes..." << std::endl;
+  std::cout << "   - Changing to COMBAT state..." << std::endl;
+  Engine::Core::EventManager::instance().publish(
+      Engine::Core::AmbientStateChangedEvent(
+          Engine::Core::AmbientState::COMBAT,
+          Engine::Core::AmbientState::PEACEFUL));
+  std::cout << "   ✓ State changed (should trigger combat music)" << std::endl;
+
+  std::cout << "   - Changing back to PEACEFUL state..." << std::endl;
+  Engine::Core::EventManager::instance().publish(
+      Engine::Core::AmbientStateChangedEvent(
+          Engine::Core::AmbientState::PEACEFUL,
+          Engine::Core::AmbientState::COMBAT));
+  std::cout << "   ✓ State changed (should trigger peaceful music)"
+            << std::endl;
+
+  std::cout << "\n9. Verifying event statistics..." << std::endl;
+  auto unitSelectedStats = Engine::Core::EventManager::instance().getStats(
+      std::type_index(typeid(Engine::Core::UnitSelectedEvent)));
+  std::cout << "   ✓ UnitSelectedEvent subscribers: "
+            << unitSelectedStats.subscriberCount << std::endl;
+  std::cout << "   ✓ UnitSelectedEvent published: "
+            << unitSelectedStats.publishCount << " times" << std::endl;
+
+  auto ambientStats = Engine::Core::EventManager::instance().getStats(
+      std::type_index(typeid(Engine::Core::AmbientStateChangedEvent)));
+  std::cout << "   ✓ AmbientStateChangedEvent subscribers: "
+            << ambientStats.subscriberCount << std::endl;
+  std::cout << "   ✓ AmbientStateChangedEvent published: "
+            << ambientStats.publishCount << " times" << std::endl;
+
+  std::cout << "\n10. Shutting down..." << std::endl;
+  handler.shutdown();
+  audioSystem.shutdown();
+  std::cout << "   ✓ All systems shutdown" << std::endl;
+
+  std::cout << "\n=== All integration tests passed! ===" << std::endl;
+  std::cout << "\nNote: Audio playback may not be audible in headless "
+               "environments."
+            << std::endl;
+
+  QTimer::singleShot(0, &app, &QCoreApplication::quit);
+  return app.exec();
+}