guess that song added
@@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Nenájdené</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Stránka nebola nájdená.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<WasmFingerprintAssets>false</WasmFingerprintAssets>
|
||||
<FingerprintAssets>false</FingerprintAssets>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.12" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\GuessTheSong.Shared\GuessTheSong.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,5 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="game-container">
|
||||
@Body
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
.game-container {
|
||||
width: 100vw;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: radial-gradient(circle at center, #1b1534 0%, #0c081e 60%, #05030e 100%);
|
||||
color: #ffffff;
|
||||
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
user-select: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
@page "/"
|
||||
@inject GameEngineService Game
|
||||
@inject AudioEngineService Audio
|
||||
@inject SongService SongSvc
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>🎵 Guess That Song – Amber & Sunset Party Edition</PageTitle>
|
||||
|
||||
<div class="game-wrapper" @ref="_gameContainer" tabindex="0" @onkeydown="HandleKeyDown" @onclick="HandleScreenClick">
|
||||
|
||||
<!-- ARCADE HEADER -->
|
||||
<header class="game-header">
|
||||
<div class="brand-container">
|
||||
<img src="logo.svg" alt="Guess That Song Logo" class="header-logo" />
|
||||
<div class="brand-title">GUESS THAT SONG</div>
|
||||
</div>
|
||||
@if (Game.Phase != GamePhase.Welcome && Game.Phase != GamePhase.GameOver)
|
||||
{
|
||||
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;">
|
||||
<div class="round-badge">
|
||||
SKLADBA #@Game.SongNumberInDeck @(Game.LoopCycle > 1 ? $"• KOLO {Game.LoopCycle}" : "")
|
||||
</div>
|
||||
<div class="score-badge">
|
||||
🏆 UHÁDNUTÉ: @Game.PartyGuessedCount / @Game.TotalPlayedCount (@Game.PartyTotalScore B)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- MAIN ARENA -->
|
||||
<main class="game-arena">
|
||||
|
||||
@if (Game.Phase == GamePhase.Welcome)
|
||||
{
|
||||
<!-- WELCOME SCREEN -->
|
||||
<div class="lobby-card" style="text-align: center; padding: 3rem 1.5rem; background: var(--card-bg); border-radius: 24px; border: 1px solid var(--card-border); box-shadow: 0 0 50px rgba(251, 133, 0, 0.25); max-width: 680px; margin: 0 auto;">
|
||||
<div style="margin-bottom: 1.2rem;">
|
||||
<img src="logo.svg" alt="Guess That Song Logo" style="width: 120px; height: 120px; animation: pulseGlow 2.2s infinite alternate;" />
|
||||
</div>
|
||||
<h1 style="font-family: 'Righteous', cursive; font-size: 3.2rem; background: linear-gradient(90deg, #ffd166, #ffb703, #fb8500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.3rem; letter-spacing: 2px;">
|
||||
GUESS THAT SONG
|
||||
</h1>
|
||||
<div style="color: var(--neon-gold); font-weight: 800; font-size: 1.1rem; letter-spacing: 4px; text-transform: uppercase; margin-bottom: 2rem;">
|
||||
AMBER & SUNSET PARTY EDITION
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(0,0,0,0.5); padding: 0.9rem 1.8rem; border-radius: 14px; max-width: 480px; margin: 0 auto 2.2rem; border: 1px solid rgba(255, 183, 3, 0.35); display: flex; justify-content: center; gap: 1.2rem; flex-wrap: wrap; font-size: 0.95rem;">
|
||||
<span><strong style="color: #fff; background: rgba(251, 133, 0, 0.25); padding: 0.2rem 0.6rem; border-radius: 6px; border: 1px solid rgba(251, 133, 0, 0.5);">SPACE</strong> = Nápoveda</span>
|
||||
<span><strong style="color: #fff; background: rgba(0, 255, 136, 0.25); padding: 0.2rem 0.6rem; border-radius: 6px; border: 1px solid rgba(0, 255, 136, 0.5);">ENTER</strong> = Uhádli sme!</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; align-items: center; flex-wrap: wrap;">
|
||||
<button class="btn-neon" style="font-size: 1.25rem; padding: 1rem 3rem;" @onclick:stopPropagation="true" @onclick="HandleStartClickAsync">
|
||||
▶ ŠTART (SPACE)
|
||||
</button>
|
||||
<button class="btn-intro-toggle" @onclick:stopPropagation="true" @onclick="ToggleIntroMusicAsync">
|
||||
@(_isIntroPlaying ? "⏸️ PAUZA INTRO" : "🎵 INTRO THEME")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- AUDIO VISUALIZER & LOOP BAR -->
|
||||
<div class="visualizer-box">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%; font-size: 0.85rem; color: var(--neon-gold);">
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem;">
|
||||
<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: @(Game.Phase == GamePhase.PlayingClue ? "var(--neon-green)" : "var(--neon-amber)"); animation: bounce 0.6s infinite alternate;"></span>
|
||||
<span style="font-weight: 800; text-transform: uppercase; letter-spacing: 1px;">
|
||||
@(Game.Phase == GamePhase.PlayingClue ? $"🔁 Hrá ukážka ({Game.CurrentClue?.AudioSnippetSec.ToString("0.0")}s v slučke)" : "⏸️ Zvuk pozastavený – tipovanie")
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn-replay-small" @onclick:stopPropagation="true" @onclick="() => Game.ReplayCurrentAudioAsync()">
|
||||
🔁 Prehrať ukážku znova (R)
|
||||
</button>
|
||||
</div>
|
||||
<canvas id="audioVisualizer" class="visualizer-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<!-- REVEAL SCREEN -->
|
||||
@if (Game.Phase == GamePhase.Revealed)
|
||||
{
|
||||
<div class="reveal-card">
|
||||
<div style="text-transform: uppercase; font-size: 0.95rem; letter-spacing: 2.5px; color: var(--neon-gold); font-weight: 900; margin-bottom: 0.5rem;">
|
||||
🎉 SPRÁVNA ODPOVEĎ
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 2.5rem; align-items: center; justify-content: center; flex-wrap: wrap; margin: 1.5rem 0;">
|
||||
@if (!string.IsNullOrEmpty(Game.CurrentSong?.CoverImage))
|
||||
{
|
||||
<div class="reveal-cover-wrapper">
|
||||
<img src="@Game.CurrentSong.CoverImage" alt="Album Cover" class="reveal-cover-img" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style="text-align: left; max-width: 500px;">
|
||||
<div class="reveal-song-title">@Game.CurrentSong?.Title</div>
|
||||
<div class="reveal-artist">Interpret: @Game.CurrentSong?.Artist</div>
|
||||
@if (!string.IsNullOrEmpty(Game.CurrentSong?.Album))
|
||||
{
|
||||
<div style="color: var(--neon-gold); font-size: 1.15rem; font-weight: 600; margin-bottom: 0.8rem;">
|
||||
💿 Album: @Game.CurrentSong.Album
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<button class="btn-neon" style="font-size: 0.95rem; padding: 0.6rem 1.6rem;" @onclick:stopPropagation="true" @onclick="() => Audio.PlayFullSongAsync(Game.CurrentSong?.AudioFile ?? string.Empty)">
|
||||
▶ Prehrať celú skladbu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SHOCKING TRIVIA & STORY -->
|
||||
@if (!string.IsNullOrEmpty(Game.CurrentSong?.ShockingTrivia))
|
||||
{
|
||||
<div class="shocking-trivia-box">
|
||||
<div class="shocking-trivia-header">
|
||||
<span>🔥 ŠOKUJÚCA PIKOŠKA ZO ZÁKULISIA:</span>
|
||||
</div>
|
||||
<div class="shocking-trivia-content">
|
||||
@Game.CurrentSong.ShockingTrivia
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Game.CurrentSong?.StorySummary))
|
||||
{
|
||||
<div class="story-summary-box">
|
||||
<div style="font-size: 0.85rem; text-transform: uppercase; color: var(--neon-gold); font-weight: 800; margin-bottom: 0.4rem; letter-spacing: 1px;">
|
||||
📖 PRÍBEH A KONTEXT SKLADBY:
|
||||
</div>
|
||||
<div style="color: #e2e8f0; font-size: 1.05rem; line-height: 1.6;">
|
||||
@Game.CurrentSong.StorySummary
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- DETAILS GRID -->
|
||||
<div class="reveal-details-grid">
|
||||
<div class="reveal-detail-item">
|
||||
<div class="reveal-detail-label">Rok vydania</div>
|
||||
<div class="reveal-detail-value">@(Game.CurrentSong?.Year > 0 ? Game.CurrentSong.Year.ToString() : "Neznámy")</div>
|
||||
</div>
|
||||
<div class="reveal-detail-item">
|
||||
<div class="reveal-detail-label">Žáner</div>
|
||||
<div class="reveal-detail-value">@Game.CurrentSong?.Genre</div>
|
||||
</div>
|
||||
<div class="reveal-detail-item">
|
||||
<div class="reveal-detail-label">Album</div>
|
||||
<div class="reveal-detail-value">@(string.IsNullOrEmpty(Game.CurrentSong?.Album) ? "Singel" : Game.CurrentSong.Album)</div>
|
||||
</div>
|
||||
<div class="reveal-detail-item">
|
||||
<div class="reveal-detail-label">Súbor</div>
|
||||
<div class="reveal-detail-value" style="font-size: 0.85rem; color: #94a3b8; word-break: break-all;">
|
||||
@Game.CurrentSong?.AudioFile
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- STAGE CLUES LIST -->
|
||||
<div class="clues-container">
|
||||
@if (Game.CurrentSong?.Clues is { Count: > 0 } clues)
|
||||
{
|
||||
@for (int i = 0; i < clues.Count; i++)
|
||||
{
|
||||
var clue = clues[i];
|
||||
bool isUnlocked = (i <= Game.CurrentStageIndex);
|
||||
bool isCurrent = (i == Game.CurrentStageIndex);
|
||||
|
||||
<div class="clue-card @(isUnlocked ? "unlocked" : "") @(isCurrent ? "current" : "")">
|
||||
<div class="clue-header">
|
||||
<span class="clue-badge">@clue.HintType</span>
|
||||
<span class="clue-points">@clue.Points @(clue.Points >= 5 ? "BODOV" : "BODY")</span>
|
||||
</div>
|
||||
<div class="clue-body">
|
||||
@if (isUnlocked)
|
||||
{
|
||||
<p style="font-weight: 500; white-space: pre-line;">@clue.Question</p>
|
||||
|
||||
@if (clue.Stage >= 2 && !string.IsNullOrEmpty(Game.CurrentSong?.CoverImage))
|
||||
{
|
||||
<div class="clue-cover-hint-box">
|
||||
<div class="clue-cover-crop clue-blur-stage-@clue.Stage">
|
||||
<img src="@Game.CurrentSong.CoverImage" alt="Cover Hint" />
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.4rem;">
|
||||
🖼️ Výrez z obalu @(clue.Stage == 2 ? "(Jemne rozostrený)" : clue.Stage == 3 ? "(Zreteľný náhľad)" : "(Kompletný obal)")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(clue.MaskedTitle))
|
||||
{
|
||||
<div class="masked-title-box">
|
||||
@clue.MaskedTitle
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: var(--text-muted); font-style: italic;">
|
||||
🔒 Indícia @(i + 1) uzamknutá (odhalí sa stlačením MEDZERNÍKA)...
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ACTION FOOTER: DUAL DOCK DURING CLUES, UNIVERSAL ON REVEAL/WELCOME -->
|
||||
@if (Game.Phase == GamePhase.PlayingClue || Game.Phase == GamePhase.Discussing)
|
||||
{
|
||||
<footer class="party-action-dock">
|
||||
<button class="dock-btn dock-btn-clue" @onclick:stopPropagation="true" @onclick="() => Game.HandleSpacebarActionAsync()">
|
||||
<div class="dock-key-tag">⎵ MEDZERNÍK</div>
|
||||
<div class="dock-btn-text">
|
||||
@(Game.CurrentStageIndex < 3 ? $"❌ NEUHÁDLI SME (ĎALŠIA NÁPOVEDA {Game.CurrentStageIndex + 2}/4)" : "❌ NEUHÁDLI SME (ODHALIŤ SKLADBU)")
|
||||
</div>
|
||||
</button>
|
||||
<button class="dock-btn dock-btn-win" @onclick:stopPropagation="true" @onclick="() => Game.HandleEnterActionAsync()">
|
||||
<div class="dock-key-tag key-enter">↵ ENTER</div>
|
||||
<div class="dock-btn-text">
|
||||
✅ UHÁDLI SME! (+@Game.CurrentPoints b.)
|
||||
</div>
|
||||
</button>
|
||||
</footer>
|
||||
}
|
||||
else
|
||||
{
|
||||
<footer class="space-action-bar" @onclick:stopPropagation="true" @onclick="() => Game.HandleSpacebarActionAsync()">
|
||||
<div class="space-key-pill">
|
||||
<span>⎵</span> MEDZERNÍK / ↵ ENTER
|
||||
</div>
|
||||
<div class="space-action-text">
|
||||
@GetSpacebarActionLabel()
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ElementReference _gameContainer;
|
||||
private bool _isIntroPlaying = false;
|
||||
|
||||
private async Task ToggleIntroMusicAsync()
|
||||
{
|
||||
if (_isIntroPlaying)
|
||||
{
|
||||
await Audio.StopAudioAsync();
|
||||
_isIntroPlaying = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Audio.InitAudioAsync();
|
||||
await Audio.PlayIntroThemeSongAsync(loop: true);
|
||||
_isIntroPlaying = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStartClickAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isIntroPlaying = false;
|
||||
await Game.StartGameAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error starting game: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Game.OnStateChanged += OnGameStateChanged;
|
||||
await Game.InitializeLobbyAsync();
|
||||
}
|
||||
|
||||
private void OnGameStateChanged()
|
||||
{
|
||||
_ = InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private bool _visualizerStarted = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gameContainer.FocusAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (Game.Phase != GamePhase.Welcome && !_visualizerStarted)
|
||||
{
|
||||
_visualizerStarted = true;
|
||||
try
|
||||
{
|
||||
await Audio.StartVisualizerAsync("audioVisualizer");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Code == "Space" || e.Key == " ")
|
||||
{
|
||||
await Game.HandleSpacebarActionAsync();
|
||||
}
|
||||
else if (e.Code is "Enter" or "NumpadEnter" or "KeyY" || e.Key is "y" or "Y")
|
||||
{
|
||||
await Game.HandleEnterActionAsync();
|
||||
}
|
||||
else if (e.Key is "r" or "R")
|
||||
{
|
||||
await Game.ReplayCurrentAudioAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in HandleKeyDown: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleScreenClick()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _gameContainer.FocusAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private string GetSpacebarActionLabel()
|
||||
{
|
||||
return Game.Phase switch
|
||||
{
|
||||
GamePhase.Welcome => "STLAČ MEDZERNÍK / ENTER PRE ŠTART ▶",
|
||||
GamePhase.PlayingClue => (Game.CurrentStageIndex < 3 ? $"MEDZERNÍK: ĎALŠIA NÁPOVEDA ({Game.CurrentStageIndex + 2}/4)" : "MEDZERNÍK: ODHALIŤ SKLADBU"),
|
||||
GamePhase.Revealed => "STLAČ MEDZERNÍK / ENTER PRE ĎALŠIU SKLADBU ▶",
|
||||
GamePhase.GameOver => "STLAČ MEDZERNÍK / ENTER PRE NOVÚ HRU",
|
||||
_ => "STLAČ MEDZERNÍK PRE POKRAČOVANIE"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Game.OnStateChanged -= OnGameStateChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using GuessTheSong.Client;
|
||||
using GuessTheSong.Client.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<AudioEngineService>();
|
||||
builder.Services.AddScoped<SongService>();
|
||||
builder.Services.AddScoped<GameEngineService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"GuessTheSong.Client": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7150;http://localhost:5150",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace GuessTheSong.Client.Services;
|
||||
|
||||
public class AudioEngineService : IAsyncDisposable
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
private IJSObjectReference? _module;
|
||||
|
||||
public AudioEngineService(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
private async ValueTask<IJSObjectReference?> GetModuleAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _module ??= await _js.InvokeAsync<IJSObjectReference>("import", "./js/audioPlayer.js");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error importing audioPlayer.js: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitAudioAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("initAudio");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"InitAudioAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlaySnippetAsync(string audioUrl, double durationSec = 1.5, double? customStartSec = null, bool autoLoop = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playSnippet", audioUrl, durationSec, customStartSec, autoLoop);
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlaySnippetAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task ReplaySnippetAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("replayLastSnippet");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"ReplaySnippetAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayFullSongAsync(string audioUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playFullSong", audioUrl);
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayFullSongAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task StopAudioAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("stopAudio");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"StopAudioAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayIntroThemeSongAsync(bool loop = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playIntroThemeSong", loop);
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayIntroThemeSongAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayArcadeIntroSoundAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playArcadeIntroSound");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayArcadeIntroSoundAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayBuzzerSoundAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playBuzzerSound");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayBuzzerSoundAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayNextClueSoundAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playNextClueSound");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayNextClueSoundAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task PlayScoreWinSoundAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("playScoreWinSound");
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"PlayScoreWinSoundAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async Task StartVisualizerAsync(string canvasId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await GetModuleAsync();
|
||||
if (module != null) await module.InvokeVoidAsync("startVisualizer", canvasId);
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"StartVisualizerAsync error: {ex.Message}"); }
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using GuessTheSong.Shared.Models;
|
||||
|
||||
namespace GuessTheSong.Client.Services;
|
||||
|
||||
public class GameEngineService
|
||||
{
|
||||
private readonly AudioEngineService _audio;
|
||||
private readonly SongService _songService;
|
||||
|
||||
public event Action? OnStateChanged;
|
||||
|
||||
public GamePhase Phase { get; private set; } = GamePhase.Welcome;
|
||||
public List<SongModel> PlayList { get; private set; } = new();
|
||||
public int CurrentSongIndex { get; private set; } = 0;
|
||||
public SongModel? CurrentSong => (CurrentSongIndex >= 0 && CurrentSongIndex < PlayList.Count) ? PlayList[CurrentSongIndex] : null;
|
||||
|
||||
public int CurrentStageIndex { get; private set; } = 0; // 0..3 (Stage 1..4)
|
||||
public ClueModel? CurrentClue => (CurrentSong?.Clues is { Count: > 0 } clues && CurrentStageIndex < clues.Count)
|
||||
? clues[CurrentStageIndex]
|
||||
: null;
|
||||
|
||||
public int CurrentPoints => CurrentClue?.Points ?? 100;
|
||||
public int TotalSongsInDeck => PlayList.Count > 0 ? PlayList.Count : _songService.SongCount;
|
||||
public int SongNumberInDeck => CurrentSongIndex + 1;
|
||||
public int TotalPlayedCount { get; private set; } = 0;
|
||||
public int LoopCycle { get; private set; } = 1;
|
||||
|
||||
// Party Score Statistics
|
||||
public int PartyGuessedCount { get; private set; } = 0;
|
||||
public int PartyTotalScore { get; private set; } = 0;
|
||||
|
||||
public GameEngineService(AudioEngineService audio, SongService songService)
|
||||
{
|
||||
_audio = audio;
|
||||
_songService = songService;
|
||||
}
|
||||
|
||||
public async Task InitializeLobbyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allSongs = await _songService.GetSongsAsync();
|
||||
if (PlayList.Count == 0 && allSongs.Count > 0)
|
||||
{
|
||||
var rand = new Random();
|
||||
PlayList = allSongs.OrderBy(_ => rand.Next()).ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in InitializeLobbyAsync: {ex}");
|
||||
}
|
||||
Notify();
|
||||
}
|
||||
|
||||
public async Task StartGameAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _audio.InitAudioAsync();
|
||||
await _audio.PlayArcadeIntroSoundAsync();
|
||||
|
||||
var allSongs = await _songService.GetSongsAsync();
|
||||
|
||||
// Fisher-Yates True Random Shuffle (bez opakovania)
|
||||
var rand = new Random();
|
||||
PlayList = allSongs.OrderBy(_ => rand.Next()).ToList();
|
||||
CurrentSongIndex = 0;
|
||||
TotalPlayedCount = 0;
|
||||
PartyGuessedCount = 0;
|
||||
PartyTotalScore = 0;
|
||||
LoopCycle = 1;
|
||||
|
||||
if (PlayList.Count > 0)
|
||||
{
|
||||
// Plynulý prechod po doznení úvodnej znelky
|
||||
await Task.Delay(850);
|
||||
await StartSongRoundAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Phase = GamePhase.Welcome;
|
||||
Notify();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in StartGameAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleSpacebarActionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _audio.InitAudioAsync();
|
||||
|
||||
switch (Phase)
|
||||
{
|
||||
case GamePhase.Welcome:
|
||||
case GamePhase.GameOver:
|
||||
await StartGameAsync();
|
||||
break;
|
||||
|
||||
case GamePhase.PlayingClue:
|
||||
case GamePhase.Discussing:
|
||||
// SPACE = Neuhádli sme -> Chceme ďalšiu nápovedu (alebo odhalenie po 4. indícii)
|
||||
await AdvanceToNextClueOrRevealAsync();
|
||||
break;
|
||||
|
||||
case GamePhase.Revealed:
|
||||
// SPACE = Posun na ďalšiu náhodnú pesničku
|
||||
await NextSongAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in HandleSpacebarActionAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleEnterActionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _audio.InitAudioAsync();
|
||||
|
||||
switch (Phase)
|
||||
{
|
||||
case GamePhase.Welcome:
|
||||
case GamePhase.GameOver:
|
||||
await StartGameAsync();
|
||||
break;
|
||||
|
||||
case GamePhase.PlayingClue:
|
||||
case GamePhase.Discussing:
|
||||
// ENTER = Uhádli sme v izbe!
|
||||
await ConfirmGuessedSuccessAsync();
|
||||
break;
|
||||
|
||||
case GamePhase.Revealed:
|
||||
// ENTER = Posun na ďalšiu pesničku
|
||||
await NextSongAsync();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in HandleEnterActionAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StartSongRoundAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
CurrentStageIndex = 0;
|
||||
Phase = GamePhase.PlayingClue;
|
||||
TotalPlayedCount++;
|
||||
Notify();
|
||||
|
||||
if (CurrentSong != null && CurrentClue != null)
|
||||
{
|
||||
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in StartSongRoundAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PauseForDiscussionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Phase = GamePhase.Discussing;
|
||||
await _audio.StopAudioAsync();
|
||||
await _audio.PlayBuzzerSoundAsync();
|
||||
Notify();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in PauseForDiscussionAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ConfirmGuessedSuccessAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
PartyGuessedCount++;
|
||||
PartyTotalScore += CurrentPoints;
|
||||
await _audio.PlayScoreWinSoundAsync();
|
||||
await RevealSongAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in ConfirmGuessedSuccessAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AdvanceToNextClueOrRevealAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (CurrentSong == null) return;
|
||||
|
||||
if (CurrentStageIndex < CurrentSong.Clues.Count - 1)
|
||||
{
|
||||
// Posun na ďalšiu indíciu
|
||||
CurrentStageIndex++;
|
||||
Phase = GamePhase.PlayingClue;
|
||||
await _audio.PlayNextClueSoundAsync();
|
||||
Notify();
|
||||
|
||||
if (CurrentClue != null)
|
||||
{
|
||||
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Vyčerpané všetky 4 indície -> Odhalenie
|
||||
await RevealSongAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in AdvanceToNextClueOrRevealAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ReplayCurrentAudioAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (CurrentSong != null && CurrentClue != null)
|
||||
{
|
||||
await _audio.PlaySnippetAsync(CurrentSong.AudioFile, CurrentClue.AudioSnippetSec, CurrentClue.CustomAudioStartSec, autoLoop: true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in ReplayCurrentAudioAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RevealSongAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Phase = GamePhase.Revealed;
|
||||
Notify();
|
||||
|
||||
if (CurrentSong != null)
|
||||
{
|
||||
await _audio.PlayFullSongAsync(CurrentSong.AudioFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in RevealSongAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NextSongAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _audio.StopAudioAsync();
|
||||
CurrentSongIndex++;
|
||||
|
||||
// NEVER-ENDING RANDOM LOOP
|
||||
if (CurrentSongIndex >= PlayList.Count)
|
||||
{
|
||||
var rand = new Random();
|
||||
PlayList = PlayList.OrderBy(_ => rand.Next()).ToList();
|
||||
CurrentSongIndex = 0;
|
||||
LoopCycle++;
|
||||
}
|
||||
|
||||
await StartSongRoundAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error in NextSongAsync: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Notify()
|
||||
{
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Net.Http.Json;
|
||||
using GuessTheSong.Shared.Helpers;
|
||||
using GuessTheSong.Shared.Models;
|
||||
|
||||
namespace GuessTheSong.Client.Services;
|
||||
|
||||
public class SongService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private List<SongModel> _songs = new();
|
||||
private bool _isLoaded;
|
||||
|
||||
public SongService(HttpClient http)
|
||||
{
|
||||
_http = http;
|
||||
}
|
||||
|
||||
public int SongCount => _songs.Count;
|
||||
|
||||
public async Task<List<SongModel>> GetSongsAsync()
|
||||
{
|
||||
if (_isLoaded && _songs.Count > 0)
|
||||
{
|
||||
return _songs;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
// Read string first to strip any BOM if present
|
||||
var jsonString = await _http.GetStringAsync("music/songs.json");
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
// Strip UTF-8 BOM if present
|
||||
if (jsonString.StartsWith("\uFEFF", StringComparison.Ordinal))
|
||||
{
|
||||
jsonString = jsonString.Substring(1);
|
||||
}
|
||||
|
||||
var loaded = System.Text.Json.JsonSerializer.Deserialize<List<SongModel>>(jsonString, options);
|
||||
if (loaded is { Count: > 0 })
|
||||
{
|
||||
foreach (var song in loaded)
|
||||
{
|
||||
var (parsedArtist, parsedTitle) = SongTitleHelper.ParseArtistAndTitle(song.AudioFile, song.Artist);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(song.Artist) && !string.IsNullOrWhiteSpace(parsedArtist))
|
||||
{
|
||||
song.Artist = parsedArtist;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(song.Title))
|
||||
{
|
||||
song.Title = parsedTitle;
|
||||
}
|
||||
else
|
||||
{
|
||||
song.Title = SongTitleHelper.CleanSongTitle(song.Title, song.Artist);
|
||||
}
|
||||
|
||||
// Auto-generate masked title if missing in Stage 4
|
||||
var lastClue = song.Clues.LastOrDefault();
|
||||
if (lastClue != null && string.IsNullOrWhiteSpace(lastClue.MaskedTitle) && !string.IsNullOrWhiteSpace(song.Title))
|
||||
{
|
||||
lastClue.MaskedTitle = SongTitleHelper.GenerateMaskedTitle(song.Title);
|
||||
}
|
||||
}
|
||||
|
||||
_songs = loaded;
|
||||
_isLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading songs.json: {ex}");
|
||||
}
|
||||
|
||||
return _songs;
|
||||
}
|
||||
|
||||
public void SetCustomSongs(List<SongModel> songs)
|
||||
{
|
||||
_songs = songs;
|
||||
_isLoaded = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using GuessTheSong.Client
|
||||
@using GuessTheSong.Client.Layout
|
||||
@using GuessTheSong.Client.Services
|
||||
@using GuessTheSong.Shared.Models
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 689 KiB |
|
After Width: | Height: | Size: 689 KiB |
@@ -0,0 +1,808 @@
|
||||
/* Game.css - Guess That Song Amber Gold & Sunset Orange Concert Lounge Theme */
|
||||
|
||||
:root {
|
||||
--bg-dark: #0a0806;
|
||||
--card-bg: rgba(22, 16, 11, 0.88);
|
||||
--card-border: rgba(255, 183, 3, 0.18);
|
||||
--neon-gold: #ffd166;
|
||||
--neon-amber: #ffb703;
|
||||
--neon-orange: #fb8500;
|
||||
--neon-fiery: #ff5400;
|
||||
--neon-yellow: #ffe600;
|
||||
--neon-green: #00ff88;
|
||||
--neon-cyan: #00e5ff;
|
||||
--text-main: #ffffff;
|
||||
--text-muted: #d4a373;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
font-family: 'Outfit', sans-serif;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
background-image:
|
||||
radial-gradient(circle at 50% 30%, rgba(18, 12, 7, 0.5) 0%, rgba(8, 5, 3, 0.92) 100%),
|
||||
url('../bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Loading screen */
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(circle at 50% 40%, rgba(36, 25, 14, 0.95) 0%, rgba(10, 8, 6, 0.98) 100%), url('../bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
padding: 1.5rem;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
background: rgba(22, 16, 11, 0.88);
|
||||
border: 1px solid rgba(255, 183, 3, 0.35);
|
||||
border-radius: 24px;
|
||||
padding: 2.5rem 2.5rem;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.8), 0 0 35px rgba(251, 133, 0, 0.25);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: fadeInScale 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.94);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-bottom: 1.2rem;
|
||||
animation: pulseGlow 1.8s infinite alternate ease-in-out;
|
||||
}
|
||||
|
||||
.loading-title {
|
||||
font-family: 'Righteous', cursive;
|
||||
font-size: 2rem;
|
||||
color: var(--neon-gold);
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 0.3rem;
|
||||
text-shadow: 0 0 20px rgba(255, 183, 3, 0.6);
|
||||
}
|
||||
|
||||
.loading-subtitle {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--neon-orange);
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 2rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.loading-progress-container {
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
background: rgba(10, 8, 6, 0.85);
|
||||
border: 1px solid rgba(255, 183, 3, 0.3);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loading-progress-bar {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, #ff5400 0%, #fb8500 40%, #ffb703 80%, #ffd166 100%);
|
||||
border-radius: 12px;
|
||||
transition: width 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
box-shadow: 0 0 16px rgba(251, 133, 0, 0.9), 0 0 25px rgba(255, 183, 3, 0.5);
|
||||
}
|
||||
|
||||
.loading-progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.35) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmerProgress 1.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes shimmerProgress {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.loading-status-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.loading-status-text {
|
||||
color: #e0d0c0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 78%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.loading-percentage-text {
|
||||
font-weight: 700;
|
||||
color: var(--neon-gold);
|
||||
text-shadow: 0 0 8px rgba(255, 209, 102, 0.5);
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
display: none;
|
||||
background: #1c1308;
|
||||
color: #ffd166;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.7);
|
||||
left: 0;
|
||||
padding: 0.8rem 1.5rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 10000;
|
||||
border-top: 1px solid var(--neon-orange);
|
||||
text-align: center;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#blazor-error-ui .reload {
|
||||
color: #ff5400;
|
||||
margin-left: 1rem;
|
||||
font-weight: 800;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@keyframes pulseGlow {
|
||||
from {
|
||||
transform: scale(0.98);
|
||||
filter: drop-shadow(0 0 15px rgba(251, 133, 0, 0.5)) drop-shadow(0 0 25px rgba(255, 209, 102, 0.4));
|
||||
}
|
||||
to {
|
||||
transform: scale(1.04);
|
||||
filter: drop-shadow(0 0 30px rgba(251, 133, 0, 0.85)) drop-shadow(0 0 50px rgba(255, 183, 3, 0.7));
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.game-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2.5rem;
|
||||
background: rgba(16, 11, 7, 0.86);
|
||||
backdrop-filter: blur(14px);
|
||||
border-bottom: 1px solid rgba(255, 183, 3, 0.18);
|
||||
}
|
||||
|
||||
.brand-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
filter: drop-shadow(0 0 10px rgba(251, 133, 0, 0.6));
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.header-logo:hover {
|
||||
transform: rotate(15deg) scale(1.1);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-family: 'Righteous', cursive;
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(90deg, #ffd166, #ffb703, #fb8500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 25px rgba(251, 133, 0, 0.5);
|
||||
}
|
||||
|
||||
.round-badge {
|
||||
background: rgba(255, 183, 3, 0.1);
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 999px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid rgba(255, 183, 3, 0.35);
|
||||
color: #ffd166;
|
||||
}
|
||||
|
||||
.score-badge {
|
||||
background: rgba(0, 255, 136, 0.15);
|
||||
border: 1px solid var(--neon-green);
|
||||
color: var(--neon-green);
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 999px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 800;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 0 15px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
/* Main arena layout */
|
||||
.game-arena {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Audio Visualizer Box */
|
||||
.visualizer-box {
|
||||
background: rgba(18, 13, 8, 0.88);
|
||||
border: 1px solid rgba(255, 183, 3, 0.3);
|
||||
border-radius: 16px;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
box-shadow: 0 0 30px rgba(251, 133, 0, 0.12);
|
||||
}
|
||||
|
||||
.visualizer-canvas {
|
||||
width: 100%;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
/* Clues Container */
|
||||
.clues-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.clue-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 18px;
|
||||
padding: 1.4rem 1.8rem;
|
||||
position: relative;
|
||||
opacity: 0.35;
|
||||
transform: scale(0.98);
|
||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.clue-card.unlocked {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
border-color: rgba(255, 183, 3, 0.35);
|
||||
}
|
||||
|
||||
.clue-card.current {
|
||||
border-color: var(--neon-amber);
|
||||
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
|
||||
background: linear-gradient(135deg, rgba(251, 133, 0, 0.12) 0%, rgba(22, 16, 11, 0.96) 100%);
|
||||
}
|
||||
|
||||
.clue-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.clue-badge {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.2px;
|
||||
font-weight: 800;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 183, 3, 0.15);
|
||||
border: 1px solid rgba(255, 183, 3, 0.25);
|
||||
color: #ffd166;
|
||||
}
|
||||
|
||||
.clue-points {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 900;
|
||||
font-size: 1.25rem;
|
||||
color: var(--neon-yellow);
|
||||
text-shadow: 0 0 10px rgba(255, 230, 0, 0.5);
|
||||
}
|
||||
|
||||
.clue-body {
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.masked-title-box {
|
||||
margin-top: 1rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 5px;
|
||||
color: var(--neon-green);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
padding: 0.8rem 1.4rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 255, 136, 0.35);
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
/* Clue Cover Visual Hints */
|
||||
.clue-cover-hint-box {
|
||||
margin: 1.2rem 0 0.5rem;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.clue-cover-crop {
|
||||
width: 165px;
|
||||
height: 165px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255, 183, 3, 0.55);
|
||||
position: relative;
|
||||
background: #000;
|
||||
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
|
||||
}
|
||||
|
||||
.clue-cover-crop img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.clue-blur-stage-2 img {
|
||||
filter: blur(3.5px) contrast(115%);
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
.clue-blur-stage-3 img {
|
||||
filter: blur(1.2px);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.clue-blur-stage-4 img {
|
||||
filter: none;
|
||||
transform: scale(1.0);
|
||||
border-color: var(--neon-green);
|
||||
}
|
||||
|
||||
/* Discussion / Tip Bar */
|
||||
.discuss-decision-bar {
|
||||
background: linear-gradient(135deg, rgba(251, 133, 0, 0.28) 0%, rgba(20, 14, 8, 0.96) 100%);
|
||||
border: 2px solid var(--neon-orange);
|
||||
border-radius: 20px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
box-shadow: 0 0 35px rgba(251, 133, 0, 0.45);
|
||||
animation: popIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.btn-decision-yes {
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
padding: 0.9rem 2.2rem;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 25px rgba(16, 185, 129, 0.5);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-decision-yes:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 35px rgba(16, 185, 129, 0.8);
|
||||
}
|
||||
|
||||
.btn-decision-no {
|
||||
background: linear-gradient(90deg, #ef4444, #dc2626);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 800;
|
||||
padding: 0.9rem 2.2rem;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 25px rgba(239, 68, 68, 0.5);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-decision-no:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 35px rgba(239, 68, 68, 0.8);
|
||||
}
|
||||
|
||||
/* Reveal Card */
|
||||
.reveal-card {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.12) 0%, rgba(22, 16, 11, 0.96) 100%);
|
||||
border: 2px solid var(--neon-green);
|
||||
border-radius: 24px;
|
||||
padding: 2.2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 45px rgba(0, 255, 136, 0.25);
|
||||
animation: popIn 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes popIn {
|
||||
from { transform: scale(0.92); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.reveal-cover-wrapper {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--neon-amber);
|
||||
box-shadow: 0 0 30px rgba(251, 133, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.reveal-cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.reveal-song-title {
|
||||
font-family: 'Righteous', cursive;
|
||||
font-size: 2.8rem;
|
||||
color: var(--neon-green);
|
||||
margin-bottom: 0.3rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.reveal-artist {
|
||||
font-size: 1.8rem;
|
||||
color: var(--neon-gold);
|
||||
margin-bottom: 0.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.reveal-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 0.8rem;
|
||||
margin: 1.5rem 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.reveal-detail-item {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 183, 3, 0.12);
|
||||
}
|
||||
|
||||
.reveal-detail-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.reveal-detail-value {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
/* Shocking Trivia & Story Summary Box */
|
||||
.shocking-trivia-box {
|
||||
margin-top: 1.4rem;
|
||||
background: linear-gradient(135deg, rgba(251, 133, 0, 0.22) 0%, rgba(255, 84, 0, 0.16) 100%);
|
||||
border: 2px solid var(--neon-orange);
|
||||
border-radius: 16px;
|
||||
padding: 1.2rem 1.6rem;
|
||||
text-align: left;
|
||||
box-shadow: 0 0 25px rgba(251, 133, 0, 0.35);
|
||||
animation: amberGlow 2.5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes amberGlow {
|
||||
from { box-shadow: 0 0 15px rgba(251, 133, 0, 0.25); }
|
||||
to { box-shadow: 0 0 35px rgba(251, 133, 0, 0.6); }
|
||||
}
|
||||
|
||||
.shocking-trivia-header {
|
||||
font-family: 'Righteous', cursive;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--neon-gold);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.shocking-trivia-content {
|
||||
font-size: 1.15rem;
|
||||
line-height: 1.6;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.story-summary-box {
|
||||
margin-top: 1rem;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(255, 183, 3, 0.3);
|
||||
border-radius: 14px;
|
||||
padding: 1.1rem 1.4rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Bottom Party Dual Action Dock (SPACE = NEXT CLUE, ENTER = GUESSED) */
|
||||
.party-action-dock {
|
||||
position: sticky;
|
||||
bottom: 1.2rem;
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dock-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.6rem;
|
||||
border-radius: 999px;
|
||||
border: 2px solid;
|
||||
backdrop-filter: blur(16px);
|
||||
cursor: pointer;
|
||||
transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dock-btn:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
}
|
||||
|
||||
.dock-btn:active {
|
||||
transform: translateY(1px) scale(0.99);
|
||||
}
|
||||
|
||||
.dock-btn-clue {
|
||||
background: rgba(26, 16, 8, 0.94);
|
||||
border-color: var(--neon-orange);
|
||||
box-shadow: 0 0 30px rgba(251, 133, 0, 0.35);
|
||||
}
|
||||
|
||||
.dock-btn-clue:hover {
|
||||
box-shadow: 0 0 45px rgba(251, 133, 0, 0.65);
|
||||
border-color: #ffd166;
|
||||
}
|
||||
|
||||
.dock-btn-win {
|
||||
background: rgba(10, 30, 20, 0.94);
|
||||
border-color: var(--neon-green);
|
||||
box-shadow: 0 0 35px rgba(0, 255, 136, 0.4);
|
||||
}
|
||||
|
||||
.dock-btn-win:hover {
|
||||
box-shadow: 0 0 55px rgba(0, 255, 136, 0.75);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.dock-key-tag {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 900;
|
||||
font-size: 1rem;
|
||||
padding: 0.45rem 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 3px 0 #d4a373;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dock-key-tag.key-enter {
|
||||
background: #00ff88;
|
||||
color: #062b16;
|
||||
box-shadow: 0 3px 0 #059669;
|
||||
}
|
||||
|
||||
.dock-btn-text {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 1.15rem;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dock-btn-clue .dock-btn-text {
|
||||
color: #ffd166;
|
||||
}
|
||||
|
||||
.dock-btn-win .dock-btn-text {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
/* Bottom Spacebar Action Footer - Ultra Radiant Arcade Prompt */
|
||||
.space-action-bar {
|
||||
position: sticky;
|
||||
bottom: 1.2rem;
|
||||
background: rgba(18, 12, 7, 0.94);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 2px solid var(--neon-amber);
|
||||
border-radius: 999px;
|
||||
padding: 1.1rem 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
box-shadow: 0 0 35px rgba(251, 133, 0, 0.4);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.space-action-bar:hover, .space-action-bar:active {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 50px rgba(251, 133, 0, 0.75);
|
||||
border-color: #ffd166;
|
||||
}
|
||||
|
||||
.space-key-pill {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 900;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.5rem 1.4rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 0 #d4a373;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.space-action-text {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: 1.5px;
|
||||
text-transform: uppercase;
|
||||
color: #ffd166;
|
||||
}
|
||||
|
||||
.btn-replay-small {
|
||||
background: rgba(255, 183, 3, 0.15);
|
||||
border: 1px solid var(--neon-amber);
|
||||
color: #ffd166;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-replay-small:hover {
|
||||
background: rgba(255, 183, 3, 0.35);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-intro-toggle {
|
||||
background: rgba(255, 183, 3, 0.12);
|
||||
border: 1px solid var(--neon-amber);
|
||||
color: var(--neon-gold);
|
||||
padding: 1.1rem 2.2rem;
|
||||
border-radius: 12px;
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
box-shadow: 0 0 15px rgba(251, 133, 0, 0.25);
|
||||
}
|
||||
|
||||
.btn-intro-toggle:hover {
|
||||
background: rgba(255, 183, 3, 0.25);
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0 25px rgba(251, 133, 0, 0.55);
|
||||
}
|
||||
|
||||
.btn-neon {
|
||||
background: linear-gradient(90deg, var(--neon-orange), var(--neon-fiery));
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 0.9rem 2.2rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 800;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 25px rgba(251, 133, 0, 0.45);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-neon:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 40px rgba(251, 133, 0, 0.85);
|
||||
}
|
||||
|
||||
.game-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,79 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<defs>
|
||||
<radialGradient id="bgGrad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#24190e"/>
|
||||
<stop offset="70%" stop-color="#140d07"/>
|
||||
<stop offset="100%" stop-color="#080503"/>
|
||||
</radialGradient>
|
||||
|
||||
<linearGradient id="amberSunset" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffd166"/>
|
||||
<stop offset="50%" stop-color="#fb8500"/>
|
||||
<stop offset="100%" stop-color="#ff5400"/>
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="goldGlowGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffe600"/>
|
||||
<stop offset="100%" stop-color="#ff9e00"/>
|
||||
</linearGradient>
|
||||
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="glow-strong" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="14" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background Circle with Gold Amber Border -->
|
||||
<circle cx="256" cy="256" r="240" fill="url(#bgGrad)" stroke="url(#amberSunset)" stroke-width="8" filter="url(#glow)"/>
|
||||
|
||||
<!-- Vinyl Grooves -->
|
||||
<circle cx="256" cy="256" r="200" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="3"/>
|
||||
<circle cx="256" cy="256" r="170" fill="none" stroke="rgba(255, 209, 102, 0.15)" stroke-width="2"/>
|
||||
<circle cx="256" cy="256" r="140" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="3"/>
|
||||
|
||||
<!-- Equalizer Amber Sunset Waveform -->
|
||||
<g filter="url(#glow)" opacity="0.95">
|
||||
<rect x="130" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
|
||||
<rect x="154" y="300" width="12" height="56" rx="6" fill="#ff6b00"/>
|
||||
<rect x="178" y="280" width="12" height="76" rx="6" fill="#fb8500"/>
|
||||
<rect x="202" y="260" width="12" height="96" rx="6" fill="#ff9e00"/>
|
||||
<rect x="226" y="235" width="12" height="121" rx="6" fill="#ffb703"/>
|
||||
<rect x="250" y="220" width="12" height="136" rx="6" fill="#ffd166"/>
|
||||
<rect x="274" y="235" width="12" height="121" rx="6" fill="#ffb703"/>
|
||||
<rect x="298" y="260" width="12" height="96" rx="6" fill="#ff9e00"/>
|
||||
<rect x="322" y="280" width="12" height="76" rx="6" fill="#fb8500"/>
|
||||
<rect x="346" y="300" width="12" height="56" rx="6" fill="#ff6b00"/>
|
||||
<rect x="370" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
|
||||
</g>
|
||||
|
||||
<!-- DJ Headphones Arc -->
|
||||
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#amberSunset)" stroke-width="18" stroke-linecap="round" filter="url(#glow-strong)"/>
|
||||
|
||||
<!-- Left Ear Cup -->
|
||||
<rect x="100" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#fb8500" stroke-width="7" filter="url(#glow)"/>
|
||||
<rect x="108" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
|
||||
|
||||
<!-- Right Ear Cup -->
|
||||
<rect x="376" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#ff5400" stroke-width="7" filter="url(#glow)"/>
|
||||
<rect x="392" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
|
||||
|
||||
<!-- Center Icon: Musical Beamed Note -->
|
||||
<ellipse cx="215" cy="235" rx="26" ry="20" transform="rotate(-20 215 235)" fill="url(#amberSunset)" filter="url(#glow)"/>
|
||||
<ellipse cx="295" cy="205" rx="26" ry="20" transform="rotate(-20 295 205)" fill="url(#amberSunset)" filter="url(#glow)"/>
|
||||
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#amberSunset)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" filter="url(#glow-strong)"/>
|
||||
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="16" stroke-linecap="round" filter="url(#glow)"/>
|
||||
|
||||
<!-- Glowing Gold Core Spark in Spindle -->
|
||||
<circle cx="256" cy="180" r="16" fill="#ffffff" stroke="#ffd166" stroke-width="4" filter="url(#glow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sk">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>🎵 Guess That Song - Amber & Sunset Edition</title>
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="64x64" href="favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800;900&family=Righteous&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/game.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="loading-screen">
|
||||
<div class="loading-box">
|
||||
<img src="logo.svg" alt="Guess That Song Logo" class="loading-logo" />
|
||||
<h2 class="loading-title">GUESS THAT SONG</h2>
|
||||
<div class="loading-subtitle">Amber & Sunset Party Edition</div>
|
||||
|
||||
<div class="loading-progress-container">
|
||||
<div id="loadingProgressBar" class="loading-progress-bar" style="width: 8%;"></div>
|
||||
</div>
|
||||
|
||||
<div class="loading-status-row">
|
||||
<span id="loadingStatusText" class="loading-status-text">Pripravujem herný engine...</span>
|
||||
<span id="loadingPercentageText" class="loading-percentage-text">8%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
Nastala neočakávaná chyba pri spúšťaní.
|
||||
<a href="" class="reload">Znovu načítať</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const bar = document.getElementById('loadingProgressBar');
|
||||
const pct = document.getElementById('loadingPercentageText');
|
||||
const statusEl = document.getElementById('loadingStatusText');
|
||||
|
||||
const statusStages = [
|
||||
{ threshold: 18, text: "Pripravujem prostredie..." },
|
||||
{ threshold: 45, text: "Sťahujem WebAssembly runtime..." },
|
||||
{ threshold: 75, text: "Načítavam herné moduly a knižnice..." },
|
||||
{ threshold: 92, text: "Inicializujem hudobný engine..." },
|
||||
{ threshold: 100, text: "Spúšťam párty..." }
|
||||
];
|
||||
|
||||
let currentProgress = 8;
|
||||
|
||||
function setProgress(val) {
|
||||
currentProgress = Math.max(currentProgress, Math.min(100, val));
|
||||
const rounded = Math.round(currentProgress);
|
||||
if (bar) bar.style.width = rounded + '%';
|
||||
if (pct) pct.textContent = rounded + '%';
|
||||
|
||||
if (statusEl) {
|
||||
const stage = statusStages.find(s => rounded <= s.threshold) || statusStages[statusStages.length - 1];
|
||||
statusEl.textContent = stage.text;
|
||||
}
|
||||
}
|
||||
|
||||
setProgress(8);
|
||||
|
||||
// 1. Observe Blazor WASM native CSS custom property if populated
|
||||
try {
|
||||
const observer = new MutationObserver(() => {
|
||||
const style = getComputedStyle(document.documentElement);
|
||||
const blazorPct = style.getPropertyValue('--blazor-load-percentage');
|
||||
if (blazorPct) {
|
||||
const parsed = parseFloat(blazorPct);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
setProgress(parsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Continuous progress updater that smoothly animates toward 95%
|
||||
const interval = setInterval(() => {
|
||||
if (currentProgress < 94) {
|
||||
const step = Math.max(0.4, (95 - currentProgress) * 0.05);
|
||||
setProgress(currentProgress + step);
|
||||
}
|
||||
}, 80);
|
||||
|
||||
window.addEventListener('unload', () => clearInterval(interval));
|
||||
})();
|
||||
</script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,473 @@
|
||||
// audioPlayer.js - Robust Web Audio API Engine for Guess That Song
|
||||
|
||||
let audioCtx = null;
|
||||
let currentSource = null;
|
||||
let gainNode = null;
|
||||
let analyserNode = null;
|
||||
let visualizerAnimationId = null;
|
||||
let audioCache = {};
|
||||
let loopTimeoutId = null;
|
||||
let isLooping = false;
|
||||
let lastPlayedParams = null;
|
||||
|
||||
function getOrCreateAudioContext() {
|
||||
try {
|
||||
if (!audioCtx) {
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContextClass) {
|
||||
audioCtx = new AudioContextClass();
|
||||
}
|
||||
}
|
||||
if (audioCtx && audioCtx.state === 'suspended') {
|
||||
audioCtx.resume().catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("AudioContext init error:", e);
|
||||
}
|
||||
return audioCtx;
|
||||
}
|
||||
|
||||
export async function initAudio() {
|
||||
try {
|
||||
getOrCreateAudioContext();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Play a short snippet with continuous repeating loop (with pause) until stopped
|
||||
export async function playSnippet(audioUrl, durationSec = 1.5, customStartSec = null, autoLoop = true, pauseBetweenSec = 1.2) {
|
||||
try {
|
||||
stopAudio();
|
||||
isLooping = autoLoop;
|
||||
lastPlayedParams = { audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec };
|
||||
|
||||
return await playSingleSnippetIteration(audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec);
|
||||
} catch (e) {
|
||||
console.warn("playSnippet error:", e);
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function replayLastSnippet() {
|
||||
try {
|
||||
if (lastPlayedParams) {
|
||||
return await playSnippet(
|
||||
lastPlayedParams.audioUrl,
|
||||
lastPlayedParams.durationSec,
|
||||
lastPlayedParams.customStartSec,
|
||||
true,
|
||||
lastPlayedParams.pauseBetweenSec
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("replayLastSnippet error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function playSingleSnippetIteration(audioUrl, durationSec, customStartSec, autoLoop, pauseBetweenSec) {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return { success: false, isSynthetic: true };
|
||||
|
||||
try {
|
||||
let buffer = audioCache[audioUrl];
|
||||
if (!buffer) {
|
||||
const fetchUrl = encodeURI(audioUrl);
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load audio from ${fetchUrl} (HTTP ${response.status})`);
|
||||
}
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
buffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
audioCache[audioUrl] = buffer;
|
||||
}
|
||||
|
||||
let startOffset = 0;
|
||||
if (customStartSec !== null && customStartSec !== undefined && customStartSec >= 0) {
|
||||
startOffset = customStartSec;
|
||||
} else {
|
||||
const maxStart = Math.max(0, buffer.duration - durationSec - 3);
|
||||
const minStart = Math.min(8, maxStart);
|
||||
startOffset = minStart + Math.random() * (maxStart - minStart);
|
||||
}
|
||||
|
||||
currentSource = ctx.createBufferSource();
|
||||
currentSource.buffer = buffer;
|
||||
|
||||
gainNode = ctx.createGain();
|
||||
const now = ctx.currentTime;
|
||||
|
||||
const fadeTime = Math.min(0.18, durationSec * 0.2);
|
||||
gainNode.gain.setValueAtTime(0, now);
|
||||
gainNode.gain.linearRampToValueAtTime(1, now + fadeTime);
|
||||
gainNode.gain.setValueAtTime(1, now + durationSec - fadeTime);
|
||||
gainNode.gain.linearRampToValueAtTime(0, now + durationSec);
|
||||
|
||||
setupAnalyser(ctx);
|
||||
|
||||
currentSource.connect(gainNode);
|
||||
gainNode.connect(analyserNode);
|
||||
analyserNode.connect(ctx.destination);
|
||||
|
||||
currentSource.start(0, startOffset, durationSec);
|
||||
|
||||
if (autoLoop && isLooping) {
|
||||
const totalCycleMs = (durationSec + pauseBetweenSec) * 1000;
|
||||
loopTimeoutId = setTimeout(() => {
|
||||
if (isLooping) {
|
||||
playSingleSnippetIteration(audioUrl, durationSec, startOffset, autoLoop, pauseBetweenSec).catch(() => {});
|
||||
}
|
||||
}, totalCycleMs);
|
||||
}
|
||||
|
||||
return { success: true, isSynthetic: false, duration: buffer.duration, startOffset };
|
||||
} catch (err) {
|
||||
console.warn("Audio file could not be played, generating synthetic preview tone:", err.message);
|
||||
playSyntheticClueTone(durationSec);
|
||||
|
||||
if (autoLoop && isLooping) {
|
||||
const totalCycleMs = (durationSec + pauseBetweenSec) * 1000;
|
||||
loopTimeoutId = setTimeout(() => {
|
||||
if (isLooping) {
|
||||
playSyntheticClueTone(durationSec);
|
||||
}
|
||||
}, totalCycleMs);
|
||||
}
|
||||
return { success: true, isSynthetic: true, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Play full song when revealed
|
||||
export async function playFullSong(audioUrl) {
|
||||
try {
|
||||
stopAudio();
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return false;
|
||||
|
||||
let buffer = audioCache[audioUrl];
|
||||
if (!buffer) {
|
||||
const fetchUrl = encodeURI(audioUrl);
|
||||
const response = await fetch(fetchUrl);
|
||||
if (!response.ok) throw new Error(`Audio not found from ${fetchUrl}`);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
buffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
audioCache[audioUrl] = buffer;
|
||||
}
|
||||
|
||||
currentSource = ctx.createBufferSource();
|
||||
currentSource.buffer = buffer;
|
||||
|
||||
gainNode = ctx.createGain();
|
||||
const now = ctx.currentTime;
|
||||
gainNode.gain.setValueAtTime(0, now);
|
||||
gainNode.gain.linearRampToValueAtTime(0.85, now + 0.8);
|
||||
|
||||
setupAnalyser(ctx);
|
||||
|
||||
currentSource.connect(gainNode);
|
||||
gainNode.connect(analyserNode);
|
||||
analyserNode.connect(ctx.destination);
|
||||
|
||||
currentSource.start(0, 0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Could not play full song:", err.message);
|
||||
playSyntheticVictoryChime();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAudio() {
|
||||
try {
|
||||
isLooping = false;
|
||||
if (loopTimeoutId) {
|
||||
clearTimeout(loopTimeoutId);
|
||||
loopTimeoutId = null;
|
||||
}
|
||||
if (currentSource) {
|
||||
try {
|
||||
currentSource.stop();
|
||||
currentSource.disconnect();
|
||||
} catch (e) { }
|
||||
currentSource = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export async function playIntroThemeSong(loop = true) {
|
||||
try {
|
||||
stopAudio();
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return false;
|
||||
|
||||
const audioUrl = "music/intro_theme.wav";
|
||||
let buffer = audioCache[audioUrl];
|
||||
if (!buffer) {
|
||||
const response = await fetch(audioUrl);
|
||||
if (!response.ok) throw new Error(`Intro audio file not found (${response.status})`);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
buffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
audioCache[audioUrl] = buffer;
|
||||
}
|
||||
|
||||
currentSource = ctx.createBufferSource();
|
||||
currentSource.buffer = buffer;
|
||||
currentSource.loop = loop;
|
||||
|
||||
gainNode = ctx.createGain();
|
||||
const now = ctx.currentTime;
|
||||
gainNode.gain.setValueAtTime(0, now);
|
||||
gainNode.gain.linearRampToValueAtTime(0.85, now + 0.4);
|
||||
|
||||
setupAnalyser(ctx);
|
||||
|
||||
currentSource.connect(gainNode);
|
||||
gainNode.connect(analyserNode);
|
||||
analyserNode.connect(ctx.destination);
|
||||
|
||||
currentSource.start(0);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn("Could not load intro WAV, playing synthetic fanfare:", err.message);
|
||||
playArcadeIntroSound();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function playArcadeIntroSound() {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const startTime = ctx.currentTime + 0.05;
|
||||
|
||||
// Synth Bass Note
|
||||
const bass = ctx.createOscillator();
|
||||
const bassGain = ctx.createGain();
|
||||
bass.type = 'sawtooth';
|
||||
bass.frequency.setValueAtTime(110, startTime);
|
||||
bass.frequency.exponentialRampToValueAtTime(55, startTime + 1.0);
|
||||
bassGain.gain.setValueAtTime(0.3, startTime);
|
||||
bassGain.gain.exponentialRampToValueAtTime(0.001, startTime + 1.0);
|
||||
bass.connect(bassGain);
|
||||
bassGain.connect(ctx.destination);
|
||||
bass.start(startTime);
|
||||
bass.stop(startTime + 1.0);
|
||||
|
||||
// Fanfare Chords
|
||||
const melody = [
|
||||
{ f: 220, t: 0.0 },
|
||||
{ f: 277.18, t: 0.10 },
|
||||
{ f: 329.63, t: 0.20 },
|
||||
{ f: 440, t: 0.30 },
|
||||
{ f: 554.37, t: 0.40 },
|
||||
{ f: 659.25, t: 0.50 },
|
||||
{ f: 880, t: 0.60 }
|
||||
];
|
||||
|
||||
melody.forEach(note => {
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
const noteStart = startTime + note.t;
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(note.f, noteStart);
|
||||
|
||||
g.gain.setValueAtTime(0.2, noteStart);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, noteStart + 0.6);
|
||||
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
|
||||
osc.start(noteStart);
|
||||
osc.stop(noteStart + 0.6);
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function playBuzzerSound() {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(140, ctx.currentTime);
|
||||
osc.frequency.linearRampToValueAtTime(90, ctx.currentTime + 0.35);
|
||||
|
||||
g.gain.setValueAtTime(0.35, ctx.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.35);
|
||||
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.35);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function playNextClueSound() {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const notes = [330, 440, 550];
|
||||
notes.forEach((freq, i) => {
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
const start = ctx.currentTime + i * 0.08;
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, start);
|
||||
|
||||
g.gain.setValueAtTime(0.2, start);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, start + 0.2);
|
||||
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
|
||||
osc.start(start);
|
||||
osc.stop(start + 0.2);
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function playScoreWinSound() {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const notes = [440, 554, 659, 880, 1108];
|
||||
notes.forEach((freq, i) => {
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
const start = ctx.currentTime + i * 0.09;
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(freq, start);
|
||||
|
||||
g.gain.setValueAtTime(0.3, start);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, start + 0.5);
|
||||
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
|
||||
osc.start(start);
|
||||
osc.stop(start + 0.5);
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function playSyntheticClueTone(durationSec) {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(220, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + durationSec);
|
||||
|
||||
g.gain.setValueAtTime(0, ctx.currentTime);
|
||||
g.gain.linearRampToValueAtTime(0.2, ctx.currentTime + 0.2);
|
||||
g.gain.setValueAtTime(0.2, ctx.currentTime + durationSec - 0.2);
|
||||
g.gain.linearRampToValueAtTime(0, ctx.currentTime + durationSec);
|
||||
|
||||
setupAnalyser(ctx);
|
||||
osc.connect(g);
|
||||
g.connect(analyserNode);
|
||||
analyserNode.connect(ctx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + durationSec);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function playSyntheticVictoryChime() {
|
||||
try {
|
||||
const ctx = getOrCreateAudioContext();
|
||||
if (!ctx) return;
|
||||
const chords = [523.25, 659.25, 783.99, 1046.50];
|
||||
chords.forEach(freq => {
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const g = ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
||||
|
||||
g.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.5);
|
||||
|
||||
osc.connect(g);
|
||||
g.connect(ctx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 1.5);
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function setupAnalyser(ctx) {
|
||||
try {
|
||||
if (!analyserNode && ctx) {
|
||||
analyserNode = ctx.createAnalyser();
|
||||
analyserNode.fftSize = 64;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function startVisualizer(canvasId) {
|
||||
try {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx2d = canvas.getContext('2d');
|
||||
|
||||
const render = () => {
|
||||
visualizerAnimationId = requestAnimationFrame(render);
|
||||
const width = canvas.width = canvas.parentElement.clientWidth || 300;
|
||||
const height = canvas.height = 70;
|
||||
|
||||
ctx2d.clearRect(0, 0, width, height);
|
||||
|
||||
if (!analyserNode) {
|
||||
ctx2d.fillStyle = 'rgba(255, 183, 3, 0.18)';
|
||||
ctx2d.fillRect(0, height / 2 - 2, width, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
const bufferLength = analyserNode.frequencyBinCount;
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
analyserNode.getByteFrequencyData(dataArray);
|
||||
|
||||
const barWidth = (width / bufferLength) * 1.5;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const barHeight = (dataArray[i] / 255) * height;
|
||||
|
||||
const grad = ctx2d.createLinearGradient(0, height - barHeight, 0, height);
|
||||
grad.addColorStop(0, '#ffd166');
|
||||
grad.addColorStop(0.5, '#ffb703');
|
||||
grad.addColorStop(1, '#fb8500');
|
||||
|
||||
ctx2d.fillStyle = grad;
|
||||
ctx2d.fillRect(x, height - barHeight, barWidth - 2, barHeight);
|
||||
|
||||
x += barWidth;
|
||||
}
|
||||
};
|
||||
|
||||
if (visualizerAnimationId) {
|
||||
cancelAnimationFrame(visualizerAnimationId);
|
||||
}
|
||||
render();
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 680 120" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#ffd166"/>
|
||||
<stop offset="50%" stop-color="#fb8500"/>
|
||||
<stop offset="100%" stop-color="#ff5400"/>
|
||||
</linearGradient>
|
||||
<filter id="textGlow" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<linearGradient id="iconGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffd166"/>
|
||||
<stop offset="100%" stop-color="#ff5400"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Left Icon Mark -->
|
||||
<g transform="translate(10, 10) scale(0.2)">
|
||||
<circle cx="256" cy="256" r="230" fill="#140e08" stroke="url(#iconGrad)" stroke-width="14" filter="url(#textGlow)"/>
|
||||
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#iconGrad)" stroke-width="24" stroke-linecap="round"/>
|
||||
<rect x="96" y="225" width="44" height="74" rx="22" fill="#24190e" stroke="#fb8500" stroke-width="8"/>
|
||||
<rect x="372" y="225" width="44" height="74" rx="22" fill="#24190e" stroke="#ff5400" stroke-width="8"/>
|
||||
<ellipse cx="215" cy="235" rx="30" ry="24" transform="rotate(-20 215 235)" fill="url(#iconGrad)"/>
|
||||
<ellipse cx="295" cy="205" rx="30" ry="24" transform="rotate(-20 295 205)" fill="url(#iconGrad)"/>
|
||||
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#iconGrad)" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="20" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Title Text -->
|
||||
<text x="130" y="65" font-family="'Righteous', 'Outfit', sans-serif" font-size="44" font-weight="900" fill="url(#textGrad)" filter="url(#textGlow)" letter-spacing="3">
|
||||
GUESS THAT SONG
|
||||
</text>
|
||||
<text x="132" y="96" font-family="'Outfit', sans-serif" font-size="15" font-weight="800" fill="#ffd166" letter-spacing="6" opacity="0.95">
|
||||
AMBER & SUNSET PARTY EDITION
|
||||
</text>
|
||||
<rect x="490" y="82" width="6" height="15" fill="#fb8500" rx="3"/>
|
||||
<text x="505" y="96" font-family="'JetBrains Mono', monospace" font-size="13" font-weight="700" fill="#cbd5e1" letter-spacing="1">
|
||||
SPACE TO PLAY
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -0,0 +1,60 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||
<defs>
|
||||
<linearGradient id="logoAmber" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ffd166"/>
|
||||
<stop offset="50%" stop-color="#fb8500"/>
|
||||
<stop offset="100%" stop-color="#ff5400"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="coreGlow" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="rgba(251, 133, 0, 0.45)"/>
|
||||
<stop offset="100%" stop-color="rgba(0, 0, 0, 0)"/>
|
||||
</radialGradient>
|
||||
<filter id="logoGlow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="8" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<circle cx="256" cy="256" r="230" fill="url(#coreGlow)"/>
|
||||
<circle cx="256" cy="256" r="220" fill="#140e08" stroke="url(#logoAmber)" stroke-width="8" filter="url(#logoGlow)"/>
|
||||
|
||||
<!-- Grooves -->
|
||||
<circle cx="256" cy="256" r="180" fill="none" stroke="rgba(255, 209, 102, 0.12)" stroke-width="2"/>
|
||||
<circle cx="256" cy="256" r="150" fill="none" stroke="rgba(255, 209, 102, 0.08)" stroke-width="2"/>
|
||||
|
||||
<!-- Equalizer -->
|
||||
<g filter="url(#logoGlow)" opacity="0.95">
|
||||
<rect x="134" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
|
||||
<rect x="158" y="295" width="12" height="61" rx="6" fill="#ff6b00"/>
|
||||
<rect x="182" y="275" width="12" height="81" rx="6" fill="#fb8500"/>
|
||||
<rect x="206" y="250" width="12" height="106" rx="6" fill="#ff9e00"/>
|
||||
<rect x="230" y="225" width="12" height="131" rx="6" fill="#ffb703"/>
|
||||
<rect x="254" y="210" width="12" height="146" rx="6" fill="#ffd166"/>
|
||||
<rect x="278" y="225" width="12" height="131" rx="6" fill="#ffb703"/>
|
||||
<rect x="302" y="250" width="12" height="106" rx="6" fill="#ff9e00"/>
|
||||
<rect x="326" y="275" width="12" height="81" rx="6" fill="#fb8500"/>
|
||||
<rect x="350" y="295" width="12" height="61" rx="6" fill="#ff6b00"/>
|
||||
<rect x="374" y="320" width="12" height="36" rx="6" fill="#ff5400"/>
|
||||
</g>
|
||||
|
||||
<!-- Headphones -->
|
||||
<path d="M 120 250 A 136 136 0 0 1 392 250" fill="none" stroke="url(#logoAmber)" stroke-width="18" stroke-linecap="round" filter="url(#logoGlow)"/>
|
||||
|
||||
<rect x="100" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#fb8500" stroke-width="7"/>
|
||||
<rect x="108" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
|
||||
|
||||
<rect x="376" y="230" width="36" height="66" rx="18" fill="#24190e" stroke="#ff5400" stroke-width="7"/>
|
||||
<rect x="392" y="240" width="12" height="46" rx="6" fill="#ffd166"/>
|
||||
|
||||
<!-- Beamed Note -->
|
||||
<ellipse cx="215" cy="235" rx="26" ry="20" transform="rotate(-20 215 235)" fill="url(#logoAmber)"/>
|
||||
<ellipse cx="295" cy="205" rx="26" ry="20" transform="rotate(-20 295 205)" fill="url(#logoAmber)"/>
|
||||
<path d="M 233 225 L 233 130 L 313 100 L 313 195" fill="none" stroke="url(#logoAmber)" stroke-width="14" stroke-linecap="round" stroke-linejoin="round" filter="url(#logoGlow)"/>
|
||||
<path d="M 230 130 L 315 100" fill="none" stroke="#fff8e7" stroke-width="16" stroke-linecap="round"/>
|
||||
|
||||
<!-- Spark -->
|
||||
<circle cx="256" cy="180" r="15" fill="#ffffff" stroke="#ffd166" stroke-width="4" filter="url(#logoGlow)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |