changes
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@using CiselnaVyprava.Components
|
||||||
|
|
||||||
@Body
|
@Body
|
||||||
|
<GlobalAudioControl />
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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.
Reference in New Issue
Block a user