@@ -11,7 +12,7 @@
- @if (HasSavedGame)
+ @if (HasSavedGame && Profile != null)
{
@code {
- [Parameter] public PlayerProfile Profile { get; set; } = new();
+ [Parameter] public PlayerProfile? Profile { get; set; }
[Parameter] public EventCallback OnNewGame { get; set; }
[Parameter] public EventCallback OnContinueGame { get; set; }
[Parameter] public EventCallback OnResetGame { get; set; }
- private bool HasSavedGame => Profile.CompletedLevels.Any() || Profile.UnlockedLevels.Count > 1;
+ private bool HasSavedGame => Profile != null && Profile.HasCreatedHero && (Profile.CompletedLevels.Any() || Profile.UnlockedLevels.Any());
private async Task ContinueGame()
{
+ _ = AudioService.PlaySfxAsync("button_click");
await OnContinueGame.InvokeAsync();
}
private async Task NewGame()
{
+ _ = AudioService.PlaySfxAsync("button_click");
await OnNewGame.InvokeAsync();
}
private async Task ConfirmReset()
{
+ _ = AudioService.PlaySfxAsync("button_click");
await OnResetGame.InvokeAsync();
}
}
diff --git a/src/CiselnaVyprava/Components/MathKeypad.razor b/src/CiselnaVyprava/Components/MathKeypad.razor
index 092b4e7..753fcd9 100644
--- a/src/CiselnaVyprava/Components/MathKeypad.razor
+++ b/src/CiselnaVyprava/Components/MathKeypad.razor
@@ -1,3 +1,5 @@
+@inject IAudioService AudioService
+
@(string.IsNullOrEmpty(CurrentValue) ? "?" : CurrentValue)
@@ -23,6 +25,7 @@
{
if (CurrentValue.Length < 6)
{
+ _ = AudioService.PlaySfxAsync("button_click");
CurrentValue += digit;
await CurrentValueChanged.InvokeAsync(CurrentValue);
}
@@ -32,6 +35,7 @@
{
if (CurrentValue.Length > 0)
{
+ _ = AudioService.PlaySfxAsync("button_click");
CurrentValue = CurrentValue.Substring(0, CurrentValue.Length - 1);
await CurrentValueChanged.InvokeAsync(CurrentValue);
}
diff --git a/src/CiselnaVyprava/Components/WorldMap.razor b/src/CiselnaVyprava/Components/WorldMap.razor
index de25d70..b189cfc 100644
--- a/src/CiselnaVyprava/Components/WorldMap.razor
+++ b/src/CiselnaVyprava/Components/WorldMap.razor
@@ -1,4 +1,5 @@
@inject IGameDataLoader DataLoader
+@inject IAudioService AudioService
OnLevelSelected { get; set; }
[Parameter] public EventCallback OnChangeHero { get; set; }
+ [Parameter] public EventCallback OnReturnToMainMenu { get; set; }
+
+ private async Task HandleReturnToMenu()
+ {
+ _ = AudioService.PlaySfxAsync("button_click");
+ await OnReturnToMainMenu.InvokeAsync();
+ }
+
+ private async Task HandleChangeHero()
+ {
+ _ = AudioService.PlaySfxAsync("button_click");
+ await OnChangeHero.InvokeAsync();
+ }
private List Worlds => DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
private int SelectedWorldIndex { get; set; } = 1;
diff --git a/src/CiselnaVyprava/Layout/MainLayout.razor b/src/CiselnaVyprava/Layout/MainLayout.razor
index e1a9a75..f92607f 100644
--- a/src/CiselnaVyprava/Layout/MainLayout.razor
+++ b/src/CiselnaVyprava/Layout/MainLayout.razor
@@ -1,3 +1,6 @@
-@inherits LayoutComponentBase
+@inherits LayoutComponentBase
+@using CiselnaVyprava.Components
@Body
+
+
diff --git a/src/CiselnaVyprava/Models/GameModels.cs b/src/CiselnaVyprava/Models/GameModels.cs
index 74fa266..5a03ab2 100644
--- a/src/CiselnaVyprava/Models/GameModels.cs
+++ b/src/CiselnaVyprava/Models/GameModels.cs
@@ -51,13 +51,14 @@ public class MathProblem
public class PlayerProfile
{
+ public bool HasCreatedHero { get; set; } = false;
public string Name { get; set; } = "Mladý Hrdina";
public int SelectedGrade { get; set; } = 2;
public string SelectedHeroId { get; set; } = "knight";
public int TotalStars { get; set; } = 0;
public int Gold { get; set; } = 0;
public int CurrentWorldId { get; set; } = 1;
- public HashSet UnlockedLevels { get; set; } = new() { "2-1-1", "3-1-1" };
+ public HashSet UnlockedLevels { get; set; } = new();
public HashSet CompletedLevels { get; set; } = new();
public PlayerStats Stats { get; set; } = new();
}
diff --git a/src/CiselnaVyprava/Pages/Home.razor b/src/CiselnaVyprava/Pages/Home.razor
index a0eb0f7..b22fbd1 100644
--- a/src/CiselnaVyprava/Pages/Home.razor
+++ b/src/CiselnaVyprava/Pages/Home.razor
@@ -1,6 +1,7 @@
@page "/"
@inject IGameDataLoader DataLoader
@inject ILocalStorageService Storage
+@inject CiselnaVyprava.Services.IAudioService AudioService
Číselná Výprava - Rozprávková Matematika
@@ -17,22 +18,23 @@
else if (CurrentScreen == ScreenType.MainMenu)
{
}
else if (CurrentScreen == ScreenType.CharacterSelect)
{
+ OnCancel="() => NavigateTo(ScreenType.MainMenu)" />
}
- else if (CurrentScreen == ScreenType.WorldMap)
+ else if (CurrentScreen == ScreenType.WorldMap && Profile != null)
{
+ OnChangeHero="() => NavigateTo(ScreenType.CharacterSelect)"
+ OnReturnToMainMenu="() => NavigateTo(ScreenType.MainMenu)" />
}
- else if (CurrentScreen == ScreenType.Battle && SelectedLevel != null)
+ else if (CurrentScreen == ScreenType.Battle && SelectedLevel != null && Profile != null)
{
var hero = DataLoader.GetHero(Profile.SelectedHeroId) ?? DataLoader.Heroes.First();
var worlds = DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
@@ -45,13 +47,14 @@
PlayerGrade="Profile.SelectedGrade"
BackgroundImage="@bg"
Config="DataLoader.Config"
- OnReturnToMap="() => CurrentScreen = ScreenType.WorldMap"
+ OnReturnToMap="() => NavigateTo(ScreenType.WorldMap)"
+ OnReturnToMainMenu="() => NavigateTo(ScreenType.MainMenu)"
OnLevelCompleted="HandleLevelCompleted"
OnNextLevelRequested="HandleNextLevelRequested" />
}
- @if (IsTransitioning && PendingLevel != null)
+ @if (IsTransitioning && PendingLevel != null && Profile != null)
{
var hero = DataLoader.GetHero(Profile.SelectedHeroId) ?? DataLoader.Heroes.FirstOrDefault();
var heroAvatar = hero != null ? $"{hero.AssetFolder}_idle_p1.png" : "assets/images/heroes/knight_idle_p1.png";
@@ -123,7 +126,7 @@
private bool IsLoading { get; set; } = true;
private ScreenType CurrentScreen { get; set; } = ScreenType.Loading;
- private PlayerProfile Profile { get; set; } = new();
+ private PlayerProfile? Profile { get; set; }
private Level? SelectedLevel { get; set; }
private Level? PendingLevel { get; set; }
@@ -135,7 +138,7 @@
{
await DataLoader.InitializeAsync();
Profile = await Storage.LoadProfileAsync();
- CurrentScreen = ScreenType.MainMenu;
+ await NavigateTo(ScreenType.MainMenu);
}
finally
{
@@ -143,18 +146,32 @@
}
}
+ private async Task NavigateTo(ScreenType screen)
+ {
+ CurrentScreen = screen;
+ if (screen is ScreenType.MainMenu or ScreenType.CharacterSelect)
+ {
+ await AudioService.PlayThemeAsync();
+ }
+ else if (screen is ScreenType.WorldMap or ScreenType.Battle)
+ {
+ await AudioService.PlayBackgroundAsync();
+ }
+ StateHasChanged();
+ }
+
private async Task HandleResetGame()
{
- Profile = new PlayerProfile();
- await Storage.SaveProfileAsync(Profile);
- StateHasChanged();
+ await Storage.ClearProfileAsync();
+ Profile = null;
+ await NavigateTo(ScreenType.MainMenu);
}
private async Task HandleProfileReady(PlayerProfile newProfile)
{
Profile = newProfile;
- CurrentScreen = ScreenType.WorldMap;
await Storage.SaveProfileAsync(Profile);
+ await NavigateTo(ScreenType.WorldMap);
}
private void HandleLevelSelected(Level level)
@@ -164,36 +181,37 @@
StateHasChanged();
}
- private void ConfirmAndStartPendingLevel()
+ private async Task ConfirmAndStartPendingLevel()
{
if (PendingLevel != null)
{
SelectedLevel = PendingLevel;
PendingLevel = null;
- CurrentScreen = ScreenType.Battle;
IsTransitioning = false;
- StateHasChanged();
+ await NavigateTo(ScreenType.Battle);
}
}
- private void CancelPendingLevel()
+ private async Task CancelPendingLevel()
{
PendingLevel = null;
IsTransitioning = false;
- CurrentScreen = ScreenType.WorldMap;
- StateHasChanged();
+ await NavigateTo(ScreenType.WorldMap);
}
private async Task HandleLevelCompleted(Level completedLevel)
{
- UnlockNextLevel(completedLevel);
- await Storage.SaveProfileAsync(Profile);
- CurrentScreen = ScreenType.WorldMap;
+ if (Profile != null)
+ {
+ UnlockNextLevel(completedLevel);
+ await Storage.SaveProfileAsync(Profile);
+ }
+ await NavigateTo(ScreenType.WorldMap);
}
private async Task HandleNextLevelRequested(Level nextLevel)
{
- if (SelectedLevel != null)
+ if (SelectedLevel != null && Profile != null)
{
UnlockNextLevel(SelectedLevel);
await Storage.SaveProfileAsync(Profile);
@@ -204,6 +222,8 @@
private void UnlockNextLevel(Level completedLevel)
{
+ if (Profile == null) return;
+
bool isFirstTime = Profile.CompletedLevels.Add(completedLevel.Id);
if (isFirstTime)
{
diff --git a/src/CiselnaVyprava/Program.cs b/src/CiselnaVyprava/Program.cs
index b7d3892..9c4535e 100644
--- a/src/CiselnaVyprava/Program.cs
+++ b/src/CiselnaVyprava/Program.cs
@@ -11,6 +11,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
await builder.Build().RunAsync();
diff --git a/src/CiselnaVyprava/Services/AudioService.cs b/src/CiselnaVyprava/Services/AudioService.cs
new file mode 100644
index 0000000..a43d882
--- /dev/null
+++ b/src/CiselnaVyprava/Services/AudioService.cs
@@ -0,0 +1,148 @@
+using Microsoft.JSInterop;
+
+namespace CiselnaVyprava.Services;
+
+public class AudioState
+{
+ public double MusicVolume { get; set; } = 0.45;
+ public bool MusicMuted { get; set; } = false;
+ public double SfxVolume { get; set; } = 0.85;
+ public bool SfxMuted { get; set; } = false;
+ public string? CurrentTrack { get; set; }
+ public bool IsPlaying { get; set; }
+}
+
+public interface IAudioService
+{
+ Task PlayThemeAsync();
+ Task PlayBackgroundAsync();
+ Task PlaySfxAsync(string sfxName, double volumeMultiplier = 1.0);
+ Task SetMusicVolumeAsync(double volume);
+ Task SetMusicMutedAsync(bool muted);
+ Task ToggleMusicMuteAsync();
+ Task SetSfxVolumeAsync(double volume);
+ Task SetSfxMutedAsync(bool muted);
+ Task ToggleSfxMuteAsync();
+ Task GetStateAsync();
+}
+
+public class AudioService : IAudioService
+{
+ private readonly IJSRuntime _js;
+
+ public AudioService(IJSRuntime js)
+ {
+ _js = js;
+ }
+
+ public async Task PlayThemeAsync()
+ {
+ try
+ {
+ await _js.InvokeVoidAsync("AudioManager.playTrack", "theme");
+ }
+ catch { }
+ }
+
+ public async Task PlayBackgroundAsync()
+ {
+ try
+ {
+ await _js.InvokeVoidAsync("AudioManager.playTrack", "background");
+ }
+ catch { }
+ }
+
+ public async Task PlaySfxAsync(string sfxName, double volumeMultiplier = 1.0)
+ {
+ try
+ {
+ await _js.InvokeVoidAsync("AudioManager.playSfx", sfxName, volumeMultiplier);
+ }
+ catch { }
+ }
+
+ public async Task SetMusicVolumeAsync(double volume)
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.setMusicVolume", volume);
+ }
+ catch
+ {
+ return new AudioState { MusicVolume = volume };
+ }
+ }
+
+ public async Task SetMusicMutedAsync(bool muted)
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.setMusicMuted", muted);
+ }
+ catch
+ {
+ return new AudioState { MusicMuted = muted };
+ }
+ }
+
+ public async Task ToggleMusicMuteAsync()
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.toggleMusicMute");
+ }
+ catch
+ {
+ return new AudioState();
+ }
+ }
+
+ public async Task SetSfxVolumeAsync(double volume)
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.setSfxVolume", volume);
+ }
+ catch
+ {
+ return new AudioState { SfxVolume = volume };
+ }
+ }
+
+ public async Task SetSfxMutedAsync(bool muted)
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.setSfxMuted", muted);
+ }
+ catch
+ {
+ return new AudioState { SfxMuted = muted };
+ }
+ }
+
+ public async Task ToggleSfxMuteAsync()
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.toggleSfxMute");
+ }
+ catch
+ {
+ return new AudioState();
+ }
+ }
+
+ public async Task GetStateAsync()
+ {
+ try
+ {
+ return await _js.InvokeAsync("AudioManager.getState");
+ }
+ catch
+ {
+ return new AudioState();
+ }
+ }
+}
diff --git a/src/CiselnaVyprava/Services/LocalStorageService.cs b/src/CiselnaVyprava/Services/LocalStorageService.cs
index 25a0ff7..f7a8901 100644
--- a/src/CiselnaVyprava/Services/LocalStorageService.cs
+++ b/src/CiselnaVyprava/Services/LocalStorageService.cs
@@ -6,8 +6,9 @@ namespace CiselnaVyprava.Services;
public interface ILocalStorageService
{
- Task LoadProfileAsync();
+ Task LoadProfileAsync();
Task SaveProfileAsync(PlayerProfile profile);
+ Task ClearProfileAsync();
}
public class LocalStorageService : ILocalStorageService
@@ -20,7 +21,7 @@ public class LocalStorageService : ILocalStorageService
_js = js;
}
- public async Task LoadProfileAsync()
+ public async Task LoadProfileAsync()
{
try
{
@@ -29,16 +30,18 @@ public class LocalStorageService : ILocalStorageService
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var profile = JsonSerializer.Deserialize(json, options);
- if (profile != null) return profile;
+ if (profile != null && profile.HasCreatedHero)
+ {
+ return profile;
+ }
}
-
}
catch
{
// Fallback upon any browser error
}
- return new PlayerProfile();
+ return null;
}
public async Task SaveProfileAsync(PlayerProfile profile)
@@ -53,4 +56,15 @@ public class LocalStorageService : ILocalStorageService
// Ignored if storage is blocked
}
}
+
+ public async Task ClearProfileAsync()
+ {
+ try
+ {
+ await _js.InvokeVoidAsync("localStorage.removeItem", ProfileStorageKey);
+ }
+ catch
+ {
+ }
+ }
}
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/background.mp3 b/src/CiselnaVyprava/wwwroot/assets/audio/background.mp3
new file mode 100644
index 0000000..d97b143
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/background.mp3 differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/arrow_shoot.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/arrow_shoot.wav
new file mode 100644
index 0000000..a7629f5
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/arrow_shoot.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/button_click.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/button_click.wav
new file mode 100644
index 0000000..217030d
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/button_click.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/coin_collect.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/coin_collect.wav
new file mode 100644
index 0000000..d65312a
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/coin_collect.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/combo_streak.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/combo_streak.wav
new file mode 100644
index 0000000..2435a7e
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/combo_streak.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/crit_hit.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/crit_hit.wav
new file mode 100644
index 0000000..4064444
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/crit_hit.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_0.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_0.wav
new file mode 100644
index 0000000..aaea65e
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_0.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_1.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_1.wav
new file mode 100644
index 0000000..bcebf02
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_1.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_2.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_2.wav
new file mode 100644
index 0000000..e98296c
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_2.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_3.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_3.wav
new file mode 100644
index 0000000..0dab053
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_3.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_4.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_4.wav
new file mode 100644
index 0000000..b89c886
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_4.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_5.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_5.wav
new file mode 100644
index 0000000..0048476
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_5.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_6.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_6.wav
new file mode 100644
index 0000000..d79ac69
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_6.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_7.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_7.wav
new file mode 100644
index 0000000..1ca34c2
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_7.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_8.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_8.wav
new file mode 100644
index 0000000..57b492f
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_8.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_9.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_9.wav
new file mode 100644
index 0000000..a012736
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/keypad_9.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/magic_cast.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/magic_cast.wav
new file mode 100644
index 0000000..575016e
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/magic_cast.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/owl_idea.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/owl_idea.wav
new file mode 100644
index 0000000..3d4989a
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/owl_idea.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/shield_block.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/shield_block.wav
new file mode 100644
index 0000000..0f8c2c9
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/shield_block.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned.wav
new file mode 100644
index 0000000..de30f62
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v1_sparkle.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v1_sparkle.wav
new file mode 100644
index 0000000..0e7fa5d
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v1_sparkle.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v2_harp.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v2_harp.wav
new file mode 100644
index 0000000..e38a3bb
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v2_harp.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v3_crystal.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v3_crystal.wav
new file mode 100644
index 0000000..de30f62
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v3_crystal.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v4_cascade.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v4_cascade.wav
new file mode 100644
index 0000000..347a217
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/star_earned_v4_cascade.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash.wav
new file mode 100644
index 0000000..68b397f
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v1_silver.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v1_silver.wav
new file mode 100644
index 0000000..268be59
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v1_silver.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v2_cartoon.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v2_cartoon.wav
new file mode 100644
index 0000000..7b93f17
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v2_cartoon.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v3_heroic.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v3_heroic.wav
new file mode 100644
index 0000000..8364493
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v3_heroic.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v4_crisp.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v4_crisp.wav
new file mode 100644
index 0000000..71da8c4
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/sword_slash_v4_crisp.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare.wav
new file mode 100644
index 0000000..8d46e00
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v1_royal.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v1_royal.wav
new file mode 100644
index 0000000..d756a0e
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v1_royal.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v2_magical.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v2_magical.wav
new file mode 100644
index 0000000..735198d
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v2_magical.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v3_playful.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v3_playful.wav
new file mode 100644
index 0000000..8d46e00
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v3_playful.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v4_golden.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v4_golden.wav
new file mode 100644
index 0000000..6002c62
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/victory_fanfare_v4_golden.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/sfx/wrong_answer.wav b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/wrong_answer.wav
new file mode 100644
index 0000000..e84a48d
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/sfx/wrong_answer.wav differ
diff --git a/src/CiselnaVyprava/wwwroot/assets/audio/theme.mp3 b/src/CiselnaVyprava/wwwroot/assets/audio/theme.mp3
new file mode 100644
index 0000000..4790f6b
Binary files /dev/null and b/src/CiselnaVyprava/wwwroot/assets/audio/theme.mp3 differ
diff --git a/src/CiselnaVyprava/wwwroot/css/app.css b/src/CiselnaVyprava/wwwroot/css/app.css
index 75e22dd..34d09c9 100644
--- a/src/CiselnaVyprava/wwwroot/css/app.css
+++ b/src/CiselnaVyprava/wwwroot/css/app.css
@@ -1385,6 +1385,19 @@ html, body {
cursor: pointer;
font-family: var(--font-main);
font-size: 0.85rem;
+ transition: all 0.15s ease;
+}
+
+.btn-back:hover {
+ background: #ffffff;
+ border-color: var(--color-gold);
+ transform: translateY(-1px);
+}
+
+.battle-header-nav {
+ display: flex;
+ align-items: center;
+ gap: 6px;
}
.world-title { font-size: clamp(0.95rem, 2vh, 1.15rem); font-weight: 700; }
@@ -2439,3 +2452,156 @@ html, body {
box-shadow: 0 2px 0 #065f46;
}
+/* ========================================================
+ GLOBAL AUDIO CONTROL WIDGET (DUAL CHANNEL: MUSIC & SFX)
+ ======================================================== */
+.global-audio-widget {
+ position: fixed;
+ top: 10px;
+ right: 12px;
+ z-index: 99999;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ pointer-events: auto;
+ user-select: none;
+}
+
+.audio-channel-pill {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: rgba(254, 250, 240, 0.95);
+ backdrop-filter: blur(8px);
+ border: 2px solid var(--color-parchment-border);
+ border-radius: 999px;
+ padding: 3px 10px 3px 5px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.22), 0 0 0 1.5px rgba(243, 156, 18, 0.2);
+ transition: all 0.2s ease;
+}
+
+.audio-channel-pill:hover {
+ background: #ffffff;
+ border-color: var(--color-gold);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3), 0 0 0 2px rgba(243, 156, 18, 0.35);
+ transform: translateY(-1px);
+}
+
+.audio-channel-pill.is-muted {
+ opacity: 0.65;
+ border-color: #bdc3c7;
+ background: #f2f4f4;
+}
+
+.channel-toggle-btn {
+ background: rgba(243, 156, 18, 0.12);
+ border: 1px solid rgba(243, 156, 18, 0.35);
+ border-radius: 999px;
+ padding: 3px 7px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ color: #5c3a21;
+ font-family: var(--font-main);
+ font-size: 11px;
+ font-weight: 800;
+ transition: all 0.15s ease;
+}
+
+.channel-toggle-btn:hover {
+ background: rgba(243, 156, 18, 0.25);
+ transform: scale(1.05);
+}
+
+.channel-toggle-btn:active {
+ transform: scale(0.95);
+}
+
+.channel-icon {
+ font-size: 13px;
+ filter: drop-shadow(0 1px 2px rgba(0,0,0,0.15));
+}
+
+.channel-label {
+ font-size: 10.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+}
+
+.channel-slider-wrap {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.channel-slider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: clamp(48px, 6vw, 68px);
+ height: 5px;
+ background: #e0d2bc;
+ border-radius: 4px;
+ outline: none;
+ cursor: pointer;
+ border: 1px solid #b89874;
+ transition: width 0.2s ease;
+}
+
+.channel-slider:hover {
+ width: clamp(58px, 7.5vw, 80px);
+}
+
+.channel-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
+ border: 1.5px solid #ffffff;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.3);
+ cursor: pointer;
+ transition: transform 0.1s ease;
+}
+
+.channel-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.25);
+}
+
+.sfx-slider::-webkit-slider-thumb {
+ background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%);
+}
+
+.channel-percent {
+ font-size: 10px;
+ font-weight: 800;
+ color: #6d4c41;
+ min-width: 26px;
+ text-align: right;
+ font-family: var(--font-main);
+}
+
+@media (max-width: 640px) {
+ .global-audio-widget {
+ top: 6px;
+ right: 6px;
+ gap: 4px;
+ transform: scale(0.88);
+ transform-origin: top right;
+ }
+
+ .channel-slider {
+ width: 38px;
+ }
+
+ .channel-slider:hover {
+ width: 46px;
+ }
+
+ .channel-label {
+ display: none;
+ }
+}
+
+
diff --git a/src/CiselnaVyprava/wwwroot/index.html b/src/CiselnaVyprava/wwwroot/index.html
index e68a94e..e4d1c0e 100644
--- a/src/CiselnaVyprava/wwwroot/index.html
+++ b/src/CiselnaVyprava/wwwroot/index.html
@@ -36,6 +36,7 @@
Obnoviť hru
🗙
+
@@ -21,7 +22,10 @@
-
@@ -186,6 +190,19 @@
[Parameter] public PlayerProfile Profile { get; set; } = new();
[Parameter] public EventCallback