This commit is contained in:
Michal Vrba
2026-09-22 15:03:10 +02:00
parent a30cc1d9d1
commit 135230f13a
56 changed files with 1275 additions and 37 deletions
@@ -1,11 +1,15 @@
@inject IGameDataLoader DataLoader @inject IGameDataLoader DataLoader
@inject IBattleEngine BattleEngine @inject IBattleEngine BattleEngine
@inject IAudioService AudioService
@implements IDisposable @implements IDisposable
<div class="battle-arena-wrapper" tabindex="0" @onkeydown="HandleKeyDown" @ref="arenaElement"> <div class="battle-arena-wrapper" tabindex="0" @onkeydown="HandleKeyDown" @ref="arenaElement">
<!-- Top HUD / Level Info --> <!-- Top HUD / Level Info -->
<div class="battle-header"> <div class="battle-header">
<button class="btn-back" @onclick="GoBackToMap">⬅ Mapa</button> <div class="battle-header-nav">
<button class="btn-back" @onclick="GoBackToMap">⬅ Mapa</button>
<button class="btn-back btn-back-menu" @onclick="GoToMainMenu">🏠 Menu</button>
</div>
<div class="level-info"> <div class="level-info">
<span class="world-title">@CurrentLevel.Name</span> <span class="world-title">@CurrentLevel.Name</span>
@if (CurrentLevel.IsBoss) @if (CurrentLevel.IsBoss)
@@ -257,6 +261,7 @@
[Parameter] public string BackgroundImage { get; set; } = "assets/images/backgrounds/world1_meadow.jpg"; [Parameter] public string BackgroundImage { get; set; } = "assets/images/backgrounds/world1_meadow.jpg";
[Parameter] public GameConfig Config { get; set; } = new(); [Parameter] public GameConfig Config { get; set; } = new();
[Parameter] public EventCallback OnReturnToMap { get; set; } [Parameter] public EventCallback OnReturnToMap { get; set; }
[Parameter] public EventCallback OnReturnToMainMenu { get; set; }
[Parameter] public EventCallback<Level> OnLevelCompleted { get; set; } [Parameter] public EventCallback<Level> OnLevelCompleted { get; set; }
[Parameter] public EventCallback<Level> OnNextLevelRequested { get; set; } [Parameter] public EventCallback<Level> OnNextLevelRequested { get; set; }
@@ -331,6 +336,8 @@
ExplanationUserAnswer = "Čas vypršal ⏱️"; ExplanationUserAnswer = "Čas vypršal ⏱️";
IsExplanationFromDefense = true; IsExplanationFromDefense = true;
ShowExplanationModal = true; ShowExplanationModal = true;
await PlayCombatSfx(result);
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
} }
else else
@@ -341,6 +348,8 @@
ExplanationUserAnswer = "Čas vypršal ⏱️"; ExplanationUserAnswer = "Čas vypršal ⏱️";
IsExplanationFromDefense = false; IsExplanationFromDefense = false;
ShowExplanationModal = true; ShowExplanationModal = true;
await PlayCombatSfx(result);
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
} }
}); });
@@ -351,6 +360,49 @@
}, null, 100, 100); }, null, 100, 100);
} }
private async Task PlayCombatSfx(CombatResultType result)
{
switch (result)
{
case CombatResultType.CriticalHit:
_ = AudioService.PlaySfxAsync("crit_hit");
if (State.ComboStreak > 1)
{
_ = Task.Delay(250).ContinueWith(_ => AudioService.PlaySfxAsync("combo_streak"));
}
break;
case CombatResultType.StandardHit:
case CombatResultType.WeakHit:
string attackSfx = "sword_slash";
if (Hero.Id.Contains("mage", StringComparison.OrdinalIgnoreCase) || Hero.AssetFolder.Contains("mage", StringComparison.OrdinalIgnoreCase))
{
attackSfx = "magic_cast";
}
else if (Hero.Id.Contains("archer", StringComparison.OrdinalIgnoreCase) || Hero.AssetFolder.Contains("archer", StringComparison.OrdinalIgnoreCase))
{
attackSfx = "arrow_shoot";
}
_ = AudioService.PlaySfxAsync(attackSfx);
if (State.ComboStreak > 1)
{
_ = Task.Delay(250).ContinueWith(_ => AudioService.PlaySfxAsync("combo_streak"));
}
break;
case CombatResultType.DefenseSuccess:
_ = AudioService.PlaySfxAsync("shield_block");
break;
case CombatResultType.PlayerMissed:
case CombatResultType.DefenseFailed:
_ = AudioService.PlaySfxAsync("wrong_answer");
break;
case CombatResultType.Victory:
_ = AudioService.PlaySfxAsync("victory_fanfare");
_ = Task.Delay(1300).ContinueWith(_ => AudioService.PlaySfxAsync("star_earned"));
_ = Task.Delay(2100).ContinueWith(_ => AudioService.PlaySfxAsync("coin_collect"));
break;
}
}
private async Task HandleInputSubmit(string input) private async Task HandleInputSubmit(string input)
{ {
if (State.IsBattleOver || ShowExplanationModal) return; if (State.IsBattleOver || ShowExplanationModal) return;
@@ -360,6 +412,7 @@
var problemBeforeSubmit = State.CurrentProblem; var problemBeforeSubmit = State.CurrentProblem;
var result = await BattleEngine.SubmitDefenseAnswerAsync(input); var result = await BattleEngine.SubmitDefenseAnswerAsync(input);
CurrentInput = string.Empty; CurrentInput = string.Empty;
await PlayCombatSfx(result);
if (result is CombatResultType.DefenseFailed or CombatResultType.Defeat) if (result is CombatResultType.DefenseFailed or CombatResultType.Defeat)
{ {
@@ -368,6 +421,7 @@
ExplanationUserAnswer = input; ExplanationUserAnswer = input;
IsExplanationFromDefense = true; IsExplanationFromDefense = true;
ShowExplanationModal = true; ShowExplanationModal = true;
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
return; return;
} }
@@ -377,6 +431,7 @@
var problemBeforeSubmit = State.CurrentProblem; var problemBeforeSubmit = State.CurrentProblem;
var result = await BattleEngine.SubmitAnswerAsync(input); var result = await BattleEngine.SubmitAnswerAsync(input);
CurrentInput = string.Empty; CurrentInput = string.Empty;
await PlayCombatSfx(result);
if (result == CombatResultType.PlayerMissed) if (result == CombatResultType.PlayerMissed)
{ {
@@ -385,6 +440,7 @@
ExplanationUserAnswer = input; ExplanationUserAnswer = input;
IsExplanationFromDefense = false; IsExplanationFromDefense = false;
ShowExplanationModal = true; ShowExplanationModal = true;
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
return; return;
} }
@@ -401,6 +457,7 @@
{ {
var problemBeforeSubmit = State.CurrentProblem; var problemBeforeSubmit = State.CurrentProblem;
var result = await BattleEngine.SubmitDefenseAnswerAsync(choice); var result = await BattleEngine.SubmitDefenseAnswerAsync(choice);
await PlayCombatSfx(result);
if (result is CombatResultType.DefenseFailed or CombatResultType.Defeat) if (result is CombatResultType.DefenseFailed or CombatResultType.Defeat)
{ {
@@ -409,6 +466,7 @@
ExplanationUserAnswer = choice; ExplanationUserAnswer = choice;
IsExplanationFromDefense = true; IsExplanationFromDefense = true;
ShowExplanationModal = true; ShowExplanationModal = true;
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
return; return;
} }
@@ -417,6 +475,7 @@
{ {
var problemBeforeSubmit = State.CurrentProblem; var problemBeforeSubmit = State.CurrentProblem;
var result = await BattleEngine.SubmitAnswerAsync(choice); var result = await BattleEngine.SubmitAnswerAsync(choice);
await PlayCombatSfx(result);
if (result == CombatResultType.PlayerMissed) if (result == CombatResultType.PlayerMissed)
{ {
@@ -425,6 +484,7 @@
ExplanationUserAnswer = choice; ExplanationUserAnswer = choice;
IsExplanationFromDefense = false; IsExplanationFromDefense = false;
ShowExplanationModal = true; ShowExplanationModal = true;
_ = AudioService.PlaySfxAsync("owl_idea");
StateHasChanged(); StateHasChanged();
return; return;
} }
@@ -435,6 +495,7 @@
private async Task DismissExplanation() private async Task DismissExplanation()
{ {
_ = AudioService.PlaySfxAsync("button_click");
ShowExplanationModal = false; ShowExplanationModal = false;
if (IsExplanationFromDefense) if (IsExplanationFromDefense)
@@ -479,17 +540,20 @@
private async Task RetryBattle() private async Task RetryBattle()
{ {
_ = AudioService.PlaySfxAsync("button_click");
StartBattle(); StartBattle();
StateHasChanged(); StateHasChanged();
} }
private async Task OnVictoryContinue() private async Task OnVictoryContinue()
{ {
_ = AudioService.PlaySfxAsync("button_click");
await OnLevelCompleted.InvokeAsync(CurrentLevel); await OnLevelCompleted.InvokeAsync(CurrentLevel);
} }
private async Task HandleNextLevelClick() private async Task HandleNextLevelClick()
{ {
_ = AudioService.PlaySfxAsync("button_click");
if (NextLevel != null) if (NextLevel != null)
{ {
await OnNextLevelRequested.InvokeAsync(NextLevel); await OnNextLevelRequested.InvokeAsync(NextLevel);
@@ -502,10 +566,18 @@
private async Task GoBackToMap() private async Task GoBackToMap()
{ {
_ = AudioService.PlaySfxAsync("button_click");
_timer?.Dispose(); _timer?.Dispose();
await OnReturnToMap.InvokeAsync(); await OnReturnToMap.InvokeAsync();
} }
private async Task GoToMainMenu()
{
_ = AudioService.PlaySfxAsync("button_click");
_timer?.Dispose();
await OnReturnToMainMenu.InvokeAsync();
}
private int EnemyHpPercent => State.EnemyMaxHp > 0 private int EnemyHpPercent => State.EnemyMaxHp > 0
? (int)((double)State.EnemyCurrentHp / State.EnemyMaxHp * 100) ? (int)((double)State.EnemyCurrentHp / State.EnemyMaxHp * 100)
: 0; : 0;
@@ -368,6 +368,7 @@
var profile = new PlayerProfile var profile = new PlayerProfile
{ {
HasCreatedHero = true,
Name = finalName, Name = finalName,
SelectedGrade = SelectedGrade, SelectedGrade = SelectedGrade,
SelectedHeroId = SelectedHeroId, SelectedHeroId = SelectedHeroId,
@@ -1,3 +1,5 @@
@inject IAudioService AudioService
<div class="choice-selector-container"> <div class="choice-selector-container">
<div class="choices-grid"> <div class="choices-grid">
@foreach (var choice in Choices) @foreach (var choice in Choices)
@@ -15,6 +17,7 @@
private async Task SelectChoice(string choice) private async Task SelectChoice(string choice)
{ {
_ = AudioService.PlaySfxAsync("button_click");
await OnChoiceSelected.InvokeAsync(choice); await OnChoiceSelected.InvokeAsync(choice);
} }
} }
@@ -0,0 +1,106 @@
@using CiselnaVyprava.Services
@inject IAudioService AudioService
<div class="global-audio-widget">
<!-- MUSIC CHANNEL -->
<div class="audio-channel-pill @(State.MusicMuted ? "is-muted" : "")">
<button class="channel-toggle-btn"
@onclick="HandleToggleMusicMute"
title="@(State.MusicMuted ? "Zapnúť hudbu" : "Stlmiť hudbu")">
<span class="channel-icon">@(State.MusicMuted ? "🔇" : "🎵")</span>
<span class="channel-label">Hudba</span>
</button>
<div class="channel-slider-wrap">
<input type="range"
min="0"
max="100"
step="5"
value="@((int)(State.MusicVolume * 100))"
@oninput="HandleMusicVolumeInput"
class="channel-slider music-slider" />
<span class="channel-percent">@((int)(State.MusicVolume * 100))%</span>
</div>
</div>
<!-- SFX CHANNEL -->
<div class="audio-channel-pill @(State.SfxMuted ? "is-muted" : "")">
<button class="channel-toggle-btn"
@onclick="HandleToggleSfxMute"
title="@(State.SfxMuted ? "Zapnúť zvuky" : "Stlmiť zvuky")">
<span class="channel-icon">@(State.SfxMuted ? "🔇" : "⚔️")</span>
<span class="channel-label">Zvuky</span>
</button>
<div class="channel-slider-wrap">
<input type="range"
min="0"
max="100"
step="5"
value="@((int)(State.SfxVolume * 100))"
@oninput="HandleSfxVolumeInput"
class="channel-slider sfx-slider" />
<span class="channel-percent">@((int)(State.SfxVolume * 100))%</span>
</div>
</div>
</div>
@code {
private AudioState State { get; set; } = new AudioState { MusicVolume = 0.45, MusicMuted = false, SfxVolume = 0.85, SfxMuted = false };
protected override async Task OnInitializedAsync()
{
try
{
State = await AudioService.GetStateAsync();
}
catch { }
}
private async Task HandleToggleMusicMute()
{
State = await AudioService.ToggleMusicMuteAsync();
StateHasChanged();
}
private async Task HandleMusicVolumeInput(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out int val))
{
double newVol = Math.Clamp(val / 100.0, 0.0, 1.0);
State.MusicVolume = newVol;
if (State.MusicMuted && newVol > 0)
{
State.MusicMuted = false;
}
State = await AudioService.SetMusicVolumeAsync(newVol);
StateHasChanged();
}
}
private async Task HandleToggleSfxMute()
{
State = await AudioService.ToggleSfxMuteAsync();
if (!State.SfxMuted)
{
_ = AudioService.PlaySfxAsync("button_click");
}
StateHasChanged();
}
private async Task HandleSfxVolumeInput(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out int val))
{
double newVol = Math.Clamp(val / 100.0, 0.0, 1.0);
State.SfxVolume = newVol;
if (State.SfxMuted && newVol > 0)
{
State.SfxMuted = false;
}
State = await AudioService.SetSfxVolumeAsync(newVol);
_ = AudioService.PlaySfxAsync("button_click");
StateHasChanged();
}
}
}
+7 -3
View File
@@ -1,4 +1,5 @@
@inject ILocalStorageService Storage @inject ILocalStorageService Storage
@inject IAudioService AudioService
<div class="main-menu-fullscreen" style="background: linear-gradient(rgba(0,0,0,0.1), rgba(0,0,0,0.4)), url('assets/images/backgrounds/title_screen_bg.jpg') center/cover no-repeat;"> <div class="main-menu-fullscreen" style="background: linear-gradient(rgba(0,0,0,0.1), rgba(0,0,0,0.4)), url('assets/images/backgrounds/title_screen_bg.jpg') center/cover no-repeat;">
<div class="main-menu-overlay"> <div class="main-menu-overlay">
@@ -11,7 +12,7 @@
<!-- PC Menu Action Box --> <!-- PC Menu Action Box -->
<div class="menu-actions-parchment"> <div class="menu-actions-parchment">
@if (HasSavedGame) @if (HasSavedGame && Profile != null)
{ {
<button class="pc-menu-btn continue-btn" @onclick="ContinueGame"> <button class="pc-menu-btn continue-btn" @onclick="ContinueGame">
<span class="btn-icon">📜</span> <span class="btn-icon">📜</span>
@@ -47,25 +48,28 @@
</div> </div>
@code { @code {
[Parameter] public PlayerProfile Profile { get; set; } = new(); [Parameter] public PlayerProfile? Profile { get; set; }
[Parameter] public EventCallback OnNewGame { get; set; } [Parameter] public EventCallback OnNewGame { get; set; }
[Parameter] public EventCallback OnContinueGame { get; set; } [Parameter] public EventCallback OnContinueGame { get; set; }
[Parameter] public EventCallback OnResetGame { 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() private async Task ContinueGame()
{ {
_ = AudioService.PlaySfxAsync("button_click");
await OnContinueGame.InvokeAsync(); await OnContinueGame.InvokeAsync();
} }
private async Task NewGame() private async Task NewGame()
{ {
_ = AudioService.PlaySfxAsync("button_click");
await OnNewGame.InvokeAsync(); await OnNewGame.InvokeAsync();
} }
private async Task ConfirmReset() private async Task ConfirmReset()
{ {
_ = AudioService.PlaySfxAsync("button_click");
await OnResetGame.InvokeAsync(); await OnResetGame.InvokeAsync();
} }
} }
@@ -1,3 +1,5 @@
@inject IAudioService AudioService
<div class="keypad-container"> <div class="keypad-container">
<div class="keypad-display"> <div class="keypad-display">
<span class="entered-value">@(string.IsNullOrEmpty(CurrentValue) ? "?" : CurrentValue)</span> <span class="entered-value">@(string.IsNullOrEmpty(CurrentValue) ? "?" : CurrentValue)</span>
@@ -23,6 +25,7 @@
{ {
if (CurrentValue.Length < 6) if (CurrentValue.Length < 6)
{ {
_ = AudioService.PlaySfxAsync("button_click");
CurrentValue += digit; CurrentValue += digit;
await CurrentValueChanged.InvokeAsync(CurrentValue); await CurrentValueChanged.InvokeAsync(CurrentValue);
} }
@@ -32,6 +35,7 @@
{ {
if (CurrentValue.Length > 0) if (CurrentValue.Length > 0)
{ {
_ = AudioService.PlaySfxAsync("button_click");
CurrentValue = CurrentValue.Substring(0, CurrentValue.Length - 1); CurrentValue = CurrentValue.Substring(0, CurrentValue.Length - 1);
await CurrentValueChanged.InvokeAsync(CurrentValue); await CurrentValueChanged.InvokeAsync(CurrentValue);
} }
+18 -1
View File
@@ -1,4 +1,5 @@
@inject IGameDataLoader DataLoader @inject IGameDataLoader DataLoader
@inject IAudioService AudioService
<div class="world-map-screen"> <div class="world-map-screen">
<!-- TOP STATUS BAR --> <!-- TOP STATUS BAR -->
@@ -21,7 +22,10 @@
</div> </div>
<div class="map-top-actions"> <div class="map-top-actions">
<button class="btn-top-action" @onclick="OnChangeHero" title="Zmeniť ročník alebo hrdinu"> <button class="btn-top-action" @onclick="HandleReturnToMenu" title="Návrat do hlavného menu">
🏠 Menu
</button>
<button class="btn-top-action" @onclick="HandleChangeHero" title="Zmeniť ročník alebo hrdinu">
🎒 Zmeniť hrdinu 🎒 Zmeniť hrdinu
</button> </button>
</div> </div>
@@ -186,6 +190,19 @@
[Parameter] public PlayerProfile Profile { get; set; } = new(); [Parameter] public PlayerProfile Profile { get; set; } = new();
[Parameter] public EventCallback<Level> OnLevelSelected { get; set; } [Parameter] public EventCallback<Level> OnLevelSelected { get; set; }
[Parameter] public EventCallback OnChangeHero { 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<World> Worlds => DataLoader.GetWorldsForGrade(Profile.SelectedGrade); private List<World> Worlds => DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
private int SelectedWorldIndex { get; set; } = 1; private int SelectedWorldIndex { get; set; } = 1;
+4 -1
View File
@@ -1,3 +1,6 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@using CiselnaVyprava.Components
@Body @Body
<GlobalAudioControl />
+2 -1
View File
@@ -51,13 +51,14 @@ public class MathProblem
public class PlayerProfile public class PlayerProfile
{ {
public bool HasCreatedHero { get; set; } = false;
public string Name { get; set; } = "Mladý Hrdina"; public string Name { get; set; } = "Mladý Hrdina";
public int SelectedGrade { get; set; } = 2; public int SelectedGrade { get; set; } = 2;
public string SelectedHeroId { get; set; } = "knight"; public string SelectedHeroId { get; set; } = "knight";
public int TotalStars { get; set; } = 0; public int TotalStars { get; set; } = 0;
public int Gold { get; set; } = 0; public int Gold { get; set; } = 0;
public int CurrentWorldId { get; set; } = 1; public int CurrentWorldId { get; set; } = 1;
public HashSet<string> UnlockedLevels { get; set; } = new() { "2-1-1", "3-1-1" }; public HashSet<string> UnlockedLevels { get; set; } = new();
public HashSet<string> CompletedLevels { get; set; } = new(); public HashSet<string> CompletedLevels { get; set; } = new();
public PlayerStats Stats { get; set; } = new(); public PlayerStats Stats { get; set; } = new();
} }
+44 -24
View File
@@ -1,6 +1,7 @@
@page "/" @page "/"
@inject IGameDataLoader DataLoader @inject IGameDataLoader DataLoader
@inject ILocalStorageService Storage @inject ILocalStorageService Storage
@inject CiselnaVyprava.Services.IAudioService AudioService
<PageTitle>Číselná Výprava - Rozprávková Matematika</PageTitle> <PageTitle>Číselná Výprava - Rozprávková Matematika</PageTitle>
@@ -17,22 +18,23 @@
else if (CurrentScreen == ScreenType.MainMenu) else if (CurrentScreen == ScreenType.MainMenu)
{ {
<MainMenu Profile="Profile" <MainMenu Profile="Profile"
OnNewGame="() => CurrentScreen = ScreenType.CharacterSelect" OnNewGame="() => NavigateTo(ScreenType.CharacterSelect)"
OnContinueGame="() => CurrentScreen = ScreenType.WorldMap" OnContinueGame="() => NavigateTo(ScreenType.WorldMap)"
OnResetGame="HandleResetGame" /> OnResetGame="HandleResetGame" />
} }
else if (CurrentScreen == ScreenType.CharacterSelect) else if (CurrentScreen == ScreenType.CharacterSelect)
{ {
<CharacterSelect OnProfileReady="HandleProfileReady" <CharacterSelect OnProfileReady="HandleProfileReady"
OnCancel="() => CurrentScreen = ScreenType.MainMenu" /> OnCancel="() => NavigateTo(ScreenType.MainMenu)" />
} }
else if (CurrentScreen == ScreenType.WorldMap) else if (CurrentScreen == ScreenType.WorldMap && Profile != null)
{ {
<WorldMap Profile="Profile" <WorldMap Profile="Profile"
OnLevelSelected="HandleLevelSelected" OnLevelSelected="HandleLevelSelected"
OnChangeHero="() => CurrentScreen = ScreenType.MainMenu" /> 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 hero = DataLoader.GetHero(Profile.SelectedHeroId) ?? DataLoader.Heroes.First();
var worlds = DataLoader.GetWorldsForGrade(Profile.SelectedGrade); var worlds = DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
@@ -45,13 +47,14 @@
PlayerGrade="Profile.SelectedGrade" PlayerGrade="Profile.SelectedGrade"
BackgroundImage="@bg" BackgroundImage="@bg"
Config="DataLoader.Config" Config="DataLoader.Config"
OnReturnToMap="() => CurrentScreen = ScreenType.WorldMap" OnReturnToMap="() => NavigateTo(ScreenType.WorldMap)"
OnReturnToMainMenu="() => NavigateTo(ScreenType.MainMenu)"
OnLevelCompleted="HandleLevelCompleted" OnLevelCompleted="HandleLevelCompleted"
OnNextLevelRequested="HandleNextLevelRequested" /> OnNextLevelRequested="HandleNextLevelRequested" />
} }
<!-- FAIRYTALE LEVEL TRANSITION SCREEN --> <!-- FAIRYTALE LEVEL TRANSITION SCREEN -->
@if (IsTransitioning && PendingLevel != null) @if (IsTransitioning && PendingLevel != null && Profile != null)
{ {
var hero = DataLoader.GetHero(Profile.SelectedHeroId) ?? DataLoader.Heroes.FirstOrDefault(); 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"; 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 bool IsLoading { get; set; } = true;
private ScreenType CurrentScreen { get; set; } = ScreenType.Loading; 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? SelectedLevel { get; set; }
private Level? PendingLevel { get; set; } private Level? PendingLevel { get; set; }
@@ -135,7 +138,7 @@
{ {
await DataLoader.InitializeAsync(); await DataLoader.InitializeAsync();
Profile = await Storage.LoadProfileAsync(); Profile = await Storage.LoadProfileAsync();
CurrentScreen = ScreenType.MainMenu; await NavigateTo(ScreenType.MainMenu);
} }
finally 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() private async Task HandleResetGame()
{ {
Profile = new PlayerProfile(); await Storage.ClearProfileAsync();
await Storage.SaveProfileAsync(Profile); Profile = null;
StateHasChanged(); await NavigateTo(ScreenType.MainMenu);
} }
private async Task HandleProfileReady(PlayerProfile newProfile) private async Task HandleProfileReady(PlayerProfile newProfile)
{ {
Profile = newProfile; Profile = newProfile;
CurrentScreen = ScreenType.WorldMap;
await Storage.SaveProfileAsync(Profile); await Storage.SaveProfileAsync(Profile);
await NavigateTo(ScreenType.WorldMap);
} }
private void HandleLevelSelected(Level level) private void HandleLevelSelected(Level level)
@@ -164,36 +181,37 @@
StateHasChanged(); StateHasChanged();
} }
private void ConfirmAndStartPendingLevel() private async Task ConfirmAndStartPendingLevel()
{ {
if (PendingLevel != null) if (PendingLevel != null)
{ {
SelectedLevel = PendingLevel; SelectedLevel = PendingLevel;
PendingLevel = null; PendingLevel = null;
CurrentScreen = ScreenType.Battle;
IsTransitioning = false; IsTransitioning = false;
StateHasChanged(); await NavigateTo(ScreenType.Battle);
} }
} }
private void CancelPendingLevel() private async Task CancelPendingLevel()
{ {
PendingLevel = null; PendingLevel = null;
IsTransitioning = false; IsTransitioning = false;
CurrentScreen = ScreenType.WorldMap; await NavigateTo(ScreenType.WorldMap);
StateHasChanged();
} }
private async Task HandleLevelCompleted(Level completedLevel) private async Task HandleLevelCompleted(Level completedLevel)
{ {
UnlockNextLevel(completedLevel); if (Profile != null)
await Storage.SaveProfileAsync(Profile); {
CurrentScreen = ScreenType.WorldMap; UnlockNextLevel(completedLevel);
await Storage.SaveProfileAsync(Profile);
}
await NavigateTo(ScreenType.WorldMap);
} }
private async Task HandleNextLevelRequested(Level nextLevel) private async Task HandleNextLevelRequested(Level nextLevel)
{ {
if (SelectedLevel != null) if (SelectedLevel != null && Profile != null)
{ {
UnlockNextLevel(SelectedLevel); UnlockNextLevel(SelectedLevel);
await Storage.SaveProfileAsync(Profile); await Storage.SaveProfileAsync(Profile);
@@ -204,6 +222,8 @@
private void UnlockNextLevel(Level completedLevel) private void UnlockNextLevel(Level completedLevel)
{ {
if (Profile == null) return;
bool isFirstTime = Profile.CompletedLevels.Add(completedLevel.Id); bool isFirstTime = Profile.CompletedLevels.Add(completedLevel.Id);
if (isFirstTime) if (isFirstTime)
{ {
+1
View File
@@ -11,6 +11,7 @@ builder.Services.AddScoped<CiselnaVyprava.Services.IGameDataLoader, CiselnaVypra
builder.Services.AddScoped<CiselnaVyprava.Services.ILocalStorageService, CiselnaVyprava.Services.LocalStorageService>(); builder.Services.AddScoped<CiselnaVyprava.Services.ILocalStorageService, CiselnaVyprava.Services.LocalStorageService>();
builder.Services.AddScoped<CiselnaVyprava.Services.IMathProblemGenerator, CiselnaVyprava.Services.MathProblemGenerator>(); builder.Services.AddScoped<CiselnaVyprava.Services.IMathProblemGenerator, CiselnaVyprava.Services.MathProblemGenerator>();
builder.Services.AddScoped<CiselnaVyprava.Services.IBattleEngine, CiselnaVyprava.Services.BattleEngine>(); builder.Services.AddScoped<CiselnaVyprava.Services.IBattleEngine, CiselnaVyprava.Services.BattleEngine>();
builder.Services.AddScoped<CiselnaVyprava.Services.IAudioService, CiselnaVyprava.Services.AudioService>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();
+148
View File
@@ -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<AudioState> SetMusicVolumeAsync(double volume);
Task<AudioState> SetMusicMutedAsync(bool muted);
Task<AudioState> ToggleMusicMuteAsync();
Task<AudioState> SetSfxVolumeAsync(double volume);
Task<AudioState> SetSfxMutedAsync(bool muted);
Task<AudioState> ToggleSfxMuteAsync();
Task<AudioState> 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<AudioState> SetMusicVolumeAsync(double volume)
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.setMusicVolume", volume);
}
catch
{
return new AudioState { MusicVolume = volume };
}
}
public async Task<AudioState> SetMusicMutedAsync(bool muted)
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.setMusicMuted", muted);
}
catch
{
return new AudioState { MusicMuted = muted };
}
}
public async Task<AudioState> ToggleMusicMuteAsync()
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.toggleMusicMute");
}
catch
{
return new AudioState();
}
}
public async Task<AudioState> SetSfxVolumeAsync(double volume)
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.setSfxVolume", volume);
}
catch
{
return new AudioState { SfxVolume = volume };
}
}
public async Task<AudioState> SetSfxMutedAsync(bool muted)
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.setSfxMuted", muted);
}
catch
{
return new AudioState { SfxMuted = muted };
}
}
public async Task<AudioState> ToggleSfxMuteAsync()
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.toggleSfxMute");
}
catch
{
return new AudioState();
}
}
public async Task<AudioState> GetStateAsync()
{
try
{
return await _js.InvokeAsync<AudioState>("AudioManager.getState");
}
catch
{
return new AudioState();
}
}
}
@@ -6,8 +6,9 @@ namespace CiselnaVyprava.Services;
public interface ILocalStorageService public interface ILocalStorageService
{ {
Task<PlayerProfile> LoadProfileAsync(); Task<PlayerProfile?> LoadProfileAsync();
Task SaveProfileAsync(PlayerProfile profile); Task SaveProfileAsync(PlayerProfile profile);
Task ClearProfileAsync();
} }
public class LocalStorageService : ILocalStorageService public class LocalStorageService : ILocalStorageService
@@ -20,7 +21,7 @@ public class LocalStorageService : ILocalStorageService
_js = js; _js = js;
} }
public async Task<PlayerProfile> LoadProfileAsync() public async Task<PlayerProfile?> LoadProfileAsync()
{ {
try try
{ {
@@ -29,16 +30,18 @@ public class LocalStorageService : ILocalStorageService
{ {
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var profile = JsonSerializer.Deserialize<PlayerProfile>(json, options); var profile = JsonSerializer.Deserialize<PlayerProfile>(json, options);
if (profile != null) return profile; if (profile != null && profile.HasCreatedHero)
{
return profile;
}
} }
} }
catch catch
{ {
// Fallback upon any browser error // Fallback upon any browser error
} }
return new PlayerProfile(); return null;
} }
public async Task SaveProfileAsync(PlayerProfile profile) public async Task SaveProfileAsync(PlayerProfile profile)
@@ -53,4 +56,15 @@ public class LocalStorageService : ILocalStorageService
// Ignored if storage is blocked // Ignored if storage is blocked
} }
} }
public async Task ClearProfileAsync()
{
try
{
await _js.InvokeVoidAsync("localStorage.removeItem", ProfileStorageKey);
}
catch
{
}
}
} }
Binary file not shown.
+166
View File
@@ -1385,6 +1385,19 @@ html, body {
cursor: pointer; cursor: pointer;
font-family: var(--font-main); font-family: var(--font-main);
font-size: 0.85rem; 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; } .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; 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;
}
}
+1
View File
@@ -36,6 +36,7 @@
<a href="." class="reload">Obnoviť hru</a> <a href="." class="reload">Obnoviť hru</a>
<span class="dismiss">🗙</span> <span class="dismiss">🗙</span>
</div> </div>
<script src="js/audioManager.js"></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body> </body>
@@ -0,0 +1,284 @@
// ČÍSELNÁ VÝPRAVA - GLOBAL AUDIO MANAGER (DUAL CHANNEL: MUSIC + SFX)
// Podpora pre plynulý fade-in (3s) a fade-out (3s), prepínanie theme/background a oddelenú kontrolu hudby aj SFX.
(function () {
const FADE_DURATION_MS = 3000;
const STORAGE_KEY = 'ciselna_vyprava_audio_config_v2';
const TRACKS = {
theme: {
url: 'assets/audio/theme.mp3',
loop: false
},
background: {
url: 'assets/audio/background.mp3',
loop: true
}
};
let musicVolume = 0.45;
let musicMuted = false;
let sfxVolume = 0.85;
let sfxMuted = false;
let hasThemePlayed = false;
let currentTrackKey = null;
let pendingTrackKey = null;
let isUnlocked = false;
// Load persisted settings
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved);
if (typeof parsed.musicVolume === 'number') musicVolume = Math.max(0, Math.min(1, parsed.musicVolume));
if (typeof parsed.musicMuted === 'boolean') musicMuted = parsed.musicMuted;
if (typeof parsed.sfxVolume === 'number') sfxVolume = Math.max(0, Math.min(1, parsed.sfxVolume));
if (typeof parsed.sfxMuted === 'boolean') sfxMuted = parsed.sfxMuted;
} else {
// Check legacy key v1
const legacy = localStorage.getItem('ciselna_vyprava_audio_config_v1');
if (legacy) {
const parsed = JSON.parse(legacy);
if (typeof parsed.volume === 'number') musicVolume = Math.max(0, Math.min(1, parsed.volume));
if (typeof parsed.muted === 'boolean') musicMuted = parsed.muted;
}
}
} catch (e) {
console.warn('Audio settings load failed', e);
}
// Audio elements for crossfading
let activeAudio = new Audio();
activeAudio.preload = 'auto';
let fadingAudio = new Audio();
fadingAudio.preload = 'auto';
let activeFadeTimer = null;
let loopMonitorInterval = null;
function saveSettings() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
musicVolume: musicVolume,
musicMuted: musicMuted,
sfxVolume: sfxVolume,
sfxMuted: sfxMuted
}));
} catch (e) {}
}
function calculateTargetMusicVolume() {
return musicMuted ? 0.0 : musicVolume;
}
function applyMusicVolumeImmediately() {
const vol = calculateTargetMusicVolume();
if (activeAudio && !activeFadeTimer) {
activeAudio.volume = vol;
}
}
function fadeVolume(audio, startVol, endVol, durationMs, onComplete) {
const startTime = performance.now();
audio.volume = startVol;
function step(now) {
const progress = Math.min(1, (now - startTime) / durationMs);
const currentVol = startVol + (endVol - startVol) * progress;
audio.volume = Math.max(0, Math.min(1, currentVol));
if (progress < 1) {
requestAnimationFrame(step);
} else {
audio.volume = endVol;
if (onComplete) onComplete();
}
}
requestAnimationFrame(step);
}
function startLoopMonitoring(audio) {
if (loopMonitorInterval) clearInterval(loopMonitorInterval);
let isDoingLoopFade = false;
loopMonitorInterval = setInterval(() => {
if (!audio || audio.paused || !audio.duration || isNaN(audio.duration)) return;
if (currentTrackKey !== 'background') return;
const remaining = audio.duration - audio.currentTime;
// Start fading out 3 seconds before the track ends
if (remaining <= 3.2 && !isDoingLoopFade && remaining > 0.3) {
isDoingLoopFade = true;
const startVol = audio.volume;
fadeVolume(audio, startVol, 0.0, 2900, () => {
if (currentTrackKey === 'background') {
audio.currentTime = 0;
const targetVol = calculateTargetMusicVolume();
fadeVolume(audio, 0.0, targetVol, FADE_DURATION_MS, () => {
isDoingLoopFade = false;
});
}
});
}
}, 250);
}
function unlockAndPlay() {
if (isUnlocked) return;
isUnlocked = true;
if (pendingTrackKey) {
playTrackInternal(pendingTrackKey);
} else if (currentTrackKey) {
playTrackInternal(currentTrackKey);
}
}
// Attach user interaction listeners for autoplay permission
['pointerdown', 'keydown', 'touchstart', 'click'].forEach(evt => {
window.addEventListener(evt, unlockAndPlay, { once: true, passive: true });
});
function playTrackInternal(trackKey) {
const track = TRACKS[trackKey];
if (!track) return;
// If theme has already played once and finishes, don't restart theme if called again
if (trackKey === 'theme' && hasThemePlayed && currentTrackKey !== 'theme') {
trackKey = 'background';
}
if (currentTrackKey === trackKey && !activeAudio.paused) {
return; // Already playing
}
currentTrackKey = trackKey;
pendingTrackKey = trackKey;
// Prepare new audio player
const newAudio = new Audio(TRACKS[trackKey].url);
newAudio.preload = 'auto';
newAudio.volume = 0.0;
newAudio.loop = TRACKS[trackKey].loop;
if (trackKey === 'theme') {
newAudio.onended = function () {
hasThemePlayed = true;
playTrackInternal('background');
};
}
// Fade out previous audio if playing
if (activeAudio && !activeAudio.paused) {
const oldAudio = activeAudio;
fadeVolume(oldAudio, oldAudio.volume, 0.0, FADE_DURATION_MS, () => {
oldAudio.pause();
oldAudio.src = '';
});
}
activeAudio = newAudio;
const targetVol = calculateTargetMusicVolume();
const playPromise = activeAudio.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isUnlocked = true;
fadeVolume(activeAudio, 0.0, targetVol, FADE_DURATION_MS, () => {
if (TRACKS[trackKey].loop) {
startLoopMonitoring(activeAudio);
}
});
}).catch(err => {
console.log('Audio autoplay waiting for user interaction:', err);
});
}
}
// SFX Playback
function playSfxInternal(sfxName, volumeMultiplier = 1.0) {
if (sfxMuted || sfxVolume <= 0) return;
const effectiveVol = Math.max(0, Math.min(1, sfxVolume * volumeMultiplier));
const url = `assets/audio/sfx/${sfxName}.wav`;
try {
const audio = new Audio(url);
audio.volume = effectiveVol;
const p = audio.play();
if (p !== undefined) {
p.catch(err => {
// Handled gracefully
});
}
} catch (e) {
console.warn('SFX play error:', sfxName, e);
}
}
window.AudioManager = {
playTrack: function (trackKey) {
playTrackInternal(trackKey);
},
playSfx: function (sfxName, volumeMultiplier) {
playSfxInternal(sfxName, volumeMultiplier);
},
// Music Controls
setMusicVolume: function (vol) {
musicVolume = Math.max(0, Math.min(1, vol));
saveSettings();
applyMusicVolumeImmediately();
return window.AudioManager.getState();
},
setMusicMuted: function (muted) {
musicMuted = !!muted;
saveSettings();
applyMusicVolumeImmediately();
return window.AudioManager.getState();
},
toggleMusicMute: function () {
musicMuted = !musicMuted;
saveSettings();
applyMusicVolumeImmediately();
return window.AudioManager.getState();
},
// SFX Controls
setSfxVolume: function (vol) {
sfxVolume = Math.max(0, Math.min(1, vol));
saveSettings();
return window.AudioManager.getState();
},
setSfxMuted: function (muted) {
sfxMuted = !!muted;
saveSettings();
return window.AudioManager.getState();
},
toggleSfxMute: function () {
sfxMuted = !sfxMuted;
saveSettings();
return window.AudioManager.getState();
},
getState: function () {
return {
musicVolume: musicVolume,
musicMuted: musicMuted,
sfxVolume: sfxVolume,
sfxMuted: sfxMuted,
currentTrack: currentTrackKey,
isPlaying: activeAudio && !activeAudio.paused
};
}
};
})();
File diff suppressed because one or more lines are too long
+16 -1
View File
@@ -48,7 +48,7 @@
<remove fileExtension=".jpeg" /> <remove fileExtension=".jpeg" />
<mimeMap fileExtension=".jpeg" mimeType="image/jpeg" /> <mimeMap fileExtension=".jpeg" mimeType="image/jpeg" />
<!-- Audio --> <!-- Audio & Media -->
<remove fileExtension=".mp3" /> <remove fileExtension=".mp3" />
<mimeMap fileExtension=".mp3" mimeType="audio/mpeg" /> <mimeMap fileExtension=".mp3" mimeType="audio/mpeg" />
@@ -57,6 +57,21 @@
<remove fileExtension=".wav" /> <remove fileExtension=".wav" />
<mimeMap fileExtension=".wav" mimeType="audio/wav" /> <mimeMap fileExtension=".wav" mimeType="audio/wav" />
<remove fileExtension=".m4a" />
<mimeMap fileExtension=".m4a" mimeType="audio/mp4" />
<remove fileExtension=".aac" />
<mimeMap fileExtension=".aac" mimeType="audio/aac" />
<remove fileExtension=".flac" />
<mimeMap fileExtension=".flac" mimeType="audio/flac" />
<remove fileExtension=".opus" />
<mimeMap fileExtension=".opus" mimeType="audio/opus" />
<remove fileExtension=".weba" />
<mimeMap fileExtension=".weba" mimeType="audio/webm" />
</staticContent> </staticContent>
<!-- SPA URL Rewrite --> <!-- SPA URL Rewrite -->
Binary file not shown.
Binary file not shown.