diff --git a/SharedProject/Models/UserSetting.cs b/SharedProject/Models/UserSetting.cs index 6020490..50808a4 100644 --- a/SharedProject/Models/UserSetting.cs +++ b/SharedProject/Models/UserSetting.cs @@ -20,9 +20,36 @@ public class UserSetting public int NotesPosition { get; set; } - public string MyDonName { get; set; } = String.Empty; + public string MyDonName { get; set; } = string.Empty; - public string Title { get; set; } = String.Empty; + public string Title { get; set; } = string.Empty; public uint TitlePlateId { get; set; } + + public uint Kigurumi { get; set; } + + public uint Head { get; set; } + + public uint Body { get; set; } + + public uint Face { get; set; } + + public uint Puchi { get; set; } + + public List UnlockedKigurumi { get; set; } = new(); + + public List UnlockedHead { get; set; } = new(); + + public List UnlockedBody { get; set; } = new(); + + public List UnlockedFace { get; set; } = new(); + + public List UnlockedPuchi { get; set; } = new(); + + public uint FaceColor { get; set; } + + public uint BodyColor { get; set; } + + public uint LimbColor { get; set; } + } \ No newline at end of file diff --git a/TaikoLocalServer.sln.DotSettings b/TaikoLocalServer.sln.DotSettings index eea663e..abd12a3 100644 --- a/TaikoLocalServer.sln.DotSettings +++ b/TaikoLocalServer.sln.DotSettings @@ -1,4 +1,6 @@  + True True True + True True \ No newline at end of file diff --git a/TaikoLocalServer/Common/Utils/JsonHelper.cs b/TaikoLocalServer/Common/Utils/JsonHelper.cs new file mode 100644 index 0000000..d409045 --- /dev/null +++ b/TaikoLocalServer/Common/Utils/JsonHelper.cs @@ -0,0 +1,52 @@ +using System.Text.Json; + +namespace TaikoLocalServer.Common.Utils; + +public static class JsonHelper +{ + public static List GetCostumeDataFromUserData(UserDatum userData, ILogger logger) + { + var costumeData = new List { 0, 0, 0, 0, 0 }; + try + { + costumeData = JsonSerializer.Deserialize>(userData.CostumeData); + } + catch (JsonException e) + { + logger.LogError(e, "Parsing costume json data failed"); + } + + if (costumeData != null && costumeData.Count >= 5) + { + return costumeData; + } + + logger.LogWarning("Costume data is null or count less than 5!"); + costumeData = new List { 0, 0, 0, 0, 0 }; + + return costumeData; + } + + public static List> GetCostumeUnlockDataFromUserData(UserDatum userData, ILogger logger) + { + var costumeUnlockData = new List> { new(), new(), new(), new(), new() }; + try + { + costumeUnlockData = JsonSerializer.Deserialize>>(userData.CostumeFlgArray); + } + catch (JsonException e) + { + logger.LogError(e, "Parsing costume json data failed"); + } + + if (costumeUnlockData != null && costumeUnlockData.Count >= 5) + { + return costumeUnlockData; + } + + logger.LogWarning("Costume unlock data is null or count less than 5!"); + costumeUnlockData = new List> { new(), new(), new(), new(), new() }; + + return costumeUnlockData; + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Common/Utils/PathHelper.cs b/TaikoLocalServer/Common/Utils/PathHelper.cs index 152d29f..9073fdc 100644 --- a/TaikoLocalServer/Common/Utils/PathHelper.cs +++ b/TaikoLocalServer/Common/Utils/PathHelper.cs @@ -2,7 +2,7 @@ public static class PathHelper { - public static string GetDataPath() + public static string GetRootPath() { var path = Environment.ProcessPath; if (path is null) @@ -14,6 +14,11 @@ public static class PathHelper { throw new ApplicationException(); } - return Path.Combine(parentPath.ToString(), "wwwroot", "data"); + return Path.Combine(parentPath.ToString(), "wwwroot"); + } + + public static string GetDataPath() + { + return Path.Combine(GetRootPath(), "data"); } } \ No newline at end of file diff --git a/TaikoLocalServer/Context/TaikoDbContext.cs b/TaikoLocalServer/Context/TaikoDbContext.cs index f6c80cb..69d41ae 100644 --- a/TaikoLocalServer/Context/TaikoDbContext.cs +++ b/TaikoLocalServer/Context/TaikoDbContext.cs @@ -22,7 +22,7 @@ { return; } - var path = Path.Combine(PathHelper.GetDataPath(), Constants.DEFAULT_DB_NAME); + var path = Path.Combine(PathHelper.GetRootPath(), Constants.DEFAULT_DB_NAME); optionsBuilder.UseSqlite($"Data Source={path}"); } diff --git a/TaikoLocalServer/Controllers/Api/UserSettingsController.cs b/TaikoLocalServer/Controllers/Api/UserSettingsController.cs index 43ac82b..40feec2 100644 --- a/TaikoLocalServer/Controllers/Api/UserSettingsController.cs +++ b/TaikoLocalServer/Controllers/Api/UserSettingsController.cs @@ -1,9 +1,11 @@ using System.Buffers.Binary; +using System.Text.Json; using SharedProject.Models; using SharedProject.Models.Responses; using SharedProject.Utils; using TaikoLocalServer.Services; using TaikoLocalServer.Services.Interfaces; +using Throw; namespace TaikoLocalServer.Controllers.Api; @@ -28,6 +30,10 @@ public class UserSettingsController : BaseController return NotFound(); } + var costumeData = JsonHelper.GetCostumeDataFromUserData(user, Logger); + + var costumeUnlockData = JsonHelper.GetCostumeUnlockDataFromUserData(user, Logger); + var response = new UserSetting { AchievementDisplayDifficulty = user.AchievementDisplayDifficulty, @@ -40,7 +46,20 @@ public class UserSettingsController : BaseController ToneId = user.SelectedToneId, MyDonName = user.MyDonName, Title = user.Title, - TitlePlateId = user.TitlePlateId + TitlePlateId = user.TitlePlateId, + Kigurumi = costumeData[0], + Head = costumeData[1], + Body = costumeData[2], + Face = costumeData[3], + Puchi = costumeData[4], + UnlockedKigurumi = costumeUnlockData[0], + UnlockedHead = costumeUnlockData[1], + UnlockedBody = costumeUnlockData[2], + UnlockedFace = costumeUnlockData[3], + UnlockedPuchi = costumeUnlockData[4], + BodyColor = user.ColorBody, + FaceColor = user.ColorFace, + LimbColor = user.ColorLimb }; return Ok(response); } @@ -55,6 +74,15 @@ public class UserSettingsController : BaseController return NotFound(); } + var costumes = new List + { + userSetting.Kigurumi, + userSetting.Head, + userSetting.Body, + userSetting.Face, + userSetting.Puchi, + }; + user.IsSkipOn = userSetting.IsSkipOn; user.IsVoiceOn = userSetting.IsVoiceOn; user.DisplayAchievement = userSetting.IsDisplayAchievement; @@ -66,6 +94,11 @@ public class UserSettingsController : BaseController user.MyDonName = userSetting.MyDonName; user.Title = userSetting.Title; user.TitlePlateId = userSetting.TitlePlateId; + user.ColorBody = userSetting.BodyColor; + user.ColorFace = userSetting.FaceColor; + user.ColorLimb = userSetting.LimbColor; + user.CostumeData = JsonSerializer.Serialize(costumes); + await userDatumService.UpdateUserDatum(user); diff --git a/TaikoLocalServer/Controllers/Game/BaidController.cs b/TaikoLocalServer/Controllers/Game/BaidController.cs index 3ffd832..32b98bd 100644 --- a/TaikoLocalServer/Controllers/Game/BaidController.cs +++ b/TaikoLocalServer/Controllers/Game/BaidController.cs @@ -1,6 +1,4 @@ -using System.Text.Json; -using TaikoLocalServer.Services.Interfaces; -using Throw; +using TaikoLocalServer.Services.Interfaces; namespace TaikoLocalServer.Controllers.Game; @@ -79,34 +77,9 @@ public class BaidController : BaseController var scoreRankCount = CalculateScoreRankCount(songCountData); - var costumeData = new List{ 0, 0, 0, 0, 0 }; - try - { - costumeData = JsonSerializer.Deserialize>(userData.CostumeData); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing costume json data failed"); - } - if (costumeData == null || costumeData.Count < 5) - { - Logger.LogWarning("Costume data is null or count less than 5!"); - costumeData = new List { 0, 0, 0, 0, 0 }; - } + var costumeData = JsonHelper.GetCostumeDataFromUserData(userData, Logger); - var costumeArrays = Array.Empty(); - try - { - costumeArrays = JsonSerializer.Deserialize(userData.CostumeFlgArray); - } - catch (JsonException e) - { - Logger.LogError(e, "Parsing costume flg json data failed"); - } - - // The only way to get a null is provide string "null" as input, - // which means database content need to be fixed, so better throw - costumeArrays.ThrowIfNull("Costume flg should never be null!"); + var costumeArrays = JsonHelper.GetCostumeUnlockDataFromUserData(userData, Logger); var costumeFlagArrays = Constants.CostumeFlagArraySizes .Select((size, index) => FlagCalculator.GetBitArrayFromIds(costumeArrays[index], size, Logger)) diff --git a/TaikoLocalServer/Entities/UserDatum.cs b/TaikoLocalServer/Entities/UserDatum.cs index 89be015..cfd98b6 100644 --- a/TaikoLocalServer/Entities/UserDatum.cs +++ b/TaikoLocalServer/Entities/UserDatum.cs @@ -6,10 +6,10 @@ public string MyDonName { get; set; } = string.Empty; public string Title { get; set; } = string.Empty; public uint TitlePlateId { get; set; } - public string FavoriteSongsArray { get; set; } = string.Empty; - public string ToneFlgArray { get; set; } = string.Empty; - public string TitleFlgArray { get; set; } = string.Empty; - public string CostumeFlgArray { get; set; } = string.Empty; + public string FavoriteSongsArray { get; set; } = "[]"; + public string ToneFlgArray { get; set; } = "[]"; + public string TitleFlgArray { get; set; } = "[]"; + public string CostumeFlgArray { get; set; } = "[]"; public short OptionSetting { get; set; } public int NotesPosition { get; set; } public bool IsVoiceOn { get; set; } @@ -20,7 +20,7 @@ public uint ColorBody { get; set; } public uint ColorFace { get; set; } public uint ColorLimb { get; set; } - public string CostumeData { get; set; } = string.Empty; + public string CostumeData { get; set; } = "[[],[],[],[],[]]"; public bool DisplayDan { get; set; } public bool DisplayAchievement { get; set; } public Difficulty AchievementDisplayDifficulty { get; set; } diff --git a/TaikoLocalServer/Program.cs b/TaikoLocalServer/Program.cs index e2bef2b..ea5cc72 100644 --- a/TaikoLocalServer/Program.cs +++ b/TaikoLocalServer/Program.cs @@ -30,7 +30,7 @@ builder.Services.AddDbContext(option => { dbName = Constants.DEFAULT_DB_NAME; } - var path = Path.Combine(PathHelper.GetDataPath(), dbName); + var path = Path.Combine(PathHelper.GetRootPath(), dbName); option.UseSqlite($"Data Source={path}"); }); builder.Services.AddHttpLogging(options => diff --git a/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs b/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs index c78d592..25d811c 100644 --- a/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs +++ b/TaikoLocalServer/Services/Interfaces/IUserDatumService.cs @@ -17,4 +17,6 @@ public interface IUserDatumService public Task> GetFavoriteSongIds(uint baid); public Task UpdateFavoriteSong(uint baid, uint songId, bool isFavorite); + + } \ No newline at end of file diff --git a/TaikoWebUI/GlobalUsings.cs b/TaikoWebUI/GlobalUsings.cs index c9a23b1..f644645 100644 --- a/TaikoWebUI/GlobalUsings.cs +++ b/TaikoWebUI/GlobalUsings.cs @@ -6,6 +6,7 @@ global using Microsoft.AspNetCore.Components.Web; global using MudBlazor; global using TaikoWebUI; global using TaikoWebUI.Services; +global using TaikoWebUI.Shared; global using SharedProject.Models; global using SharedProject.Models.Requests; global using SharedProject.Models.Responses; diff --git a/TaikoWebUI/Pages/Dialogs/ChooseTitleDialog.razor b/TaikoWebUI/Pages/Dialogs/ChooseTitleDialog.razor new file mode 100644 index 0000000..3a10ea1 --- /dev/null +++ b/TaikoWebUI/Pages/Dialogs/ChooseTitleDialog.razor @@ -0,0 +1,94 @@ +@using TaikoWebUI.Shared.Models +@using System.Collections.Immutable +@inject IGameDataService GameDataService + + + + + + + + + + + + + + + + ID + + + + + Title + + + + + @context.TitleId + @context.TitleName + + + + + + Selected Title: @selectedTitle?.TitleName + + + Cancel + Ok + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public UserSetting UserSetting { get; set; } = new(); + + private IEnumerable titles = new List<Title>(); + + private Title? selectedTitle; + + private string searchString = string.Empty; + + protected override void OnInitialized() + { + base.OnInitialized(); + var titleSet = GameDataService.GetTitles(); + titles = titleSet.ToImmutableList().Sort((title, title1) => title.TitleId.CompareTo(title1.TitleId)); + var currentTitle = new Title + { + TitleName = UserSetting.Title + }; + if (titleSet.Contains(currentTitle)) + { + titleSet.TryGetValue(new Title + { + TitleName = UserSetting.Title + }, out selectedTitle); + } + } + + private bool Filter(Title title) + { + return string.IsNullOrEmpty(searchString) || + title.TitleName.Contains(searchString, StringComparison.InvariantCultureIgnoreCase); + } + + private void Submit() + { + if (selectedTitle is not null) + { + UserSetting.Title = selectedTitle.TitleName; + } + MudDialog.Close(DialogResult.Ok(true)); + } + + private void Cancel() => MudDialog.Cancel(); + +} \ No newline at end of file diff --git a/TaikoWebUI/Pages/Profile.razor b/TaikoWebUI/Pages/Profile.razor index a70d29e..1373290 100644 --- a/TaikoWebUI/Pages/Profile.razor +++ b/TaikoWebUI/Pages/Profile.razor @@ -1,5 +1,7 @@ @page "/Cards/{baid:int}/Profile" @inject HttpClient Client +@inject IGameDataService GameDataService +@inject IDialogService DialogService <MudBreadcrumbs Items="breadcrumbs" Class="px-0"></MudBreadcrumbs> @@ -8,107 +10,205 @@ @if (response is not null) { - <MudGrid> - <MudItem xs="12" md="8"> - <MudPaper Class="py-8 px-8 my-8" Outlined="true"> - <MudStack Spacing="4"> - <h2>Profile Options</h2> + <MudGrid Class="my-4 pb-10"> + <MudItem xs="12" md="8"> + <MudPaper Elevation="0" Outlined="true"> + <MudTabs Rounded="true" Border="true" PanelClass="pa-8"> + <MudTabPanel Text="Profile"> + <MudStack Spacing="4"> + <h2>Profile Options</h2> - <MudTextField @bind-Value="@response.MyDonName" Label="Name"></MudTextField> + <MudTextField @bind-Value="@response.MyDonName" Label="Name"></MudTextField> - <MudGrid> - <MudItem xs="12" md="8"> - <MudTextField @bind-Value="@response.Title" Label="Title"></MudTextField> - </MudItem> - <MudItem xs="12" md="4"> - <MudSelect @bind-Value="@response.TitlePlateId" Label="Title Plate"> - @for (uint i = 0; i < 8; i++) - { - var index = i; - <MudSelectItem Value="@i">@titlePlateStrings[index]</MudSelectItem> - } - </MudSelect> - </MudItem> - </MudGrid> - - <MudSelect @bind-Value="@response.AchievementDisplayDifficulty" - Label="Achievement Panel Difficulty"> - @foreach (var item in Enum.GetValues<Difficulty>()) - { - <MudSelectItem Value="@item"/> - } - </MudSelect> - - <MudSwitch @bind-Checked="@response.IsDisplayAchievement" Label="Display Achievement Panel" Color="Color.Primary"/> - <MudSwitch @bind-Checked="@response.IsDisplayDanOnNamePlate" Label="Display Dan Rank on Name Plate" Color="Color.Primary"/> - </MudStack> - </MudPaper> - - <MudPaper Class="py-8 px-8 my-8" Outlined="true"> - <MudStack Spacing="4"> - <h2>Song Options</h2> - <MudGrid> - <MudItem xs="12" md="4"> - <MudStack Spacing="4"> - <MudSwitch @bind-Checked="@response.PlaySetting.IsVanishOn" Label="Vanish" Color="Color.Primary"/> - <MudSwitch @bind-Checked="@response.PlaySetting.IsInverseOn" Label="Inverse" Color="Color.Primary"/> - <MudSwitch @bind-Checked="@response.IsSkipOn" Label="Give Up" Color="Color.Primary"/> - <MudSwitch @bind-Checked="@response.IsVoiceOn" Label="Voice" Color="Color.Primary"/> - </MudStack> - </MudItem> - <MudItem xs="12" md="8"> - <MudStack Spacing="4"> - <MudSelect @bind-Value="@response.PlaySetting.Speed" Label="Speed"> - @for (uint i = 0; i < 15; i++) + <MudGrid> + <MudItem xs="12" md="8"> + <MudTextField @bind-Value="@response.Title" Label="Title"/> + <MudButton Color="Color.Primary" Class="mt-1" Size="Size.Small" OnClick="@((e)=>OpenChooseTitleDialog())"> + Select a Title + </MudButton> + </MudItem> + <MudItem xs="12" md="4"> + <MudSelect @bind-Value="@response.TitlePlateId" Label="Title Plate"> + @for (uint i = 0; i < 8; i++) { var index = i; - <MudSelectItem Value="@i">@speedStrings[index]</MudSelectItem> + <MudSelectItem Value="@i">@TitlePlateStrings[index]</MudSelectItem> } </MudSelect> + </MudItem> + </MudGrid> - <MudSelect @bind-Value="@response.PlaySetting.RandomType" - Label="Random"> - @foreach (var item in Enum.GetValues<RandomType>()) - { - <MudSelectItem Value="@item"/> - } - </MudSelect> + <MudSelect @bind-Value="@response.AchievementDisplayDifficulty" + Label="Achievement Panel Difficulty"> + @foreach (var item in Enum.GetValues<Difficulty>()) + { + <MudSelectItem Value="@item"/> + } + </MudSelect> - <MudSelect @bind-Value="@response.ToneId" Label="Tone"> - @for (uint i = 0; i < 19; i++) - { - var index = i; - <MudSelectItem Value="@i">@toneStrings[index]</MudSelectItem> - } - </MudSelect> + <MudSwitch @bind-Checked="@response.IsDisplayAchievement" Label="Display Achievement Panel" Color="Color.Primary"/> + <MudSwitch @bind-Checked="@response.IsDisplayDanOnNamePlate" Label="Display Dan Rank on Name Plate" Color="Color.Primary"/> + </MudStack> + </MudTabPanel> - <MudSlider Class="mb-8" @bind-Value="@response.NotesPosition" Size="Size.Medium" Min="-5" Max="5" Step="1" TickMarks="true" TickMarkLabels="@notePositionStrings"> - <MudText Typo="Typo.caption">Notes Position</MudText> - </MudSlider> - </MudStack> - </MudItem> - </MudGrid> - </MudStack> - </MudPaper> - </MudItem> - <MudItem md="4" xs="12" Class="py-8 px-8 my-4 pt-8"> - <MudStack Spacing="4" Style="top:100px" Class="sticky"> - <MudButton Disabled="@isSavingOptions" - OnClick="SaveOptions" - Variant="Variant.Filled" - Color="Color.Primary"> - @if (isSavingOptions) - { - <MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/> - <MudText Class="ms-2">Saving...</MudText> - } - else - { - <MudIcon Icon="@Icons.Filled.Save" Class="mx-2"></MudIcon> - <MudText>Save</MudText> - } - </MudButton> - </MudStack> - </MudItem> + <MudTabPanel Text="Costume"> + <MudStack Spacing="4"> + <h2>Costume Options</h2> + <MudGrid> + <MudItem xs="12"> + <MudStack Spacing="4" Class="mb-8"> + <MudSelect @bind-Value="@response.Head" Label="Head"> + @for (var i = 0; i < Constants.COSTUME_HEAD_MAX; i++) + { + var index = (uint)i; + var costumeTitle = GameDataService.GetHeadTitle(index); + <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem> + } + </MudSelect> + + <MudSelect @bind-Value="@response.Body" Label="Body"> + @for (var i = 0; i < Constants.COSTUME_BODY_MAX; i++) + { + var index = (uint)i; + var costumeTitle = GameDataService.GetBodyTitle(index); + <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem> + } + </MudSelect> + + <MudSelect @bind-Value="@response.Face" Label="Face"> + @for (var i = 0; i < Constants.COSTUME_FACE_MAX; i++) + { + var index = (uint)i; + var costumeTitle = GameDataService.GetFaceTitle(index); + <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem> + } + </MudSelect> + + <MudSelect @bind-Value="@response.Kigurumi" Label="Kigurumi"> + @for (var i = 0; i < Constants.COSTUME_KIGURUMI_MAX; i++) + { + var index = (uint)i; + var costumeTitle = GameDataService.GetKigurumiTitle(index); + <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem> + } + </MudSelect> + + <MudSelect @bind-Value="@response.Puchi" Label="Puchi"> + @for (var i = 0; i < Constants.COSTUME_PUCHI_MAX; i++) + { + var index = (uint)i; + var costumeTitle = GameDataService.GetPuchiTitle(index); + <MudSelectItem Value="@index">@index - @costumeTitle</MudSelectItem> + } + </MudSelect> + </MudStack> + + <MudStack Row="true"> + <MudSelect @bind-Value="@response.BodyColor" Label="Body Color"> + @for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++) + { + var index = i; + <MudSelectItem Value="@index"> + <div class="color-box" style=@($"background: {CostumeColors[index]}")></div> + @index + </MudSelectItem> + } + </MudSelect> + <MudSelect @bind-Value="@response.FaceColor" Label="Face Color"> + @for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++) + { + var index = i; + <MudSelectItem Value="@index"> + <div class="color-box" style=@($"background: {CostumeColors[index]}")></div> + @index + </MudSelectItem> + } + </MudSelect> + <MudSelect @bind-Value="@response.LimbColor" Label="Limb Color"> + @for (uint i = 0; i < Constants.COSTUME_COLOR_MAX; i++) + { + var index = i; + <MudSelectItem Value="@index"> + <div class="color-box" style=@($"background: {CostumeColors[index]}")></div> + @index + </MudSelectItem> + } + </MudSelect> + </MudStack> + </MudItem> + </MudGrid> + </MudStack> + </MudTabPanel> + + <MudTabPanel Text="Song Options"> + <MudStack Spacing="4"> + <h2>Song Options</h2> + <MudGrid> + <MudItem xs="12" md="4"> + <MudStack Spacing="4"> + <MudSwitch @bind-Checked="@response.PlaySetting.IsVanishOn" Label="Vanish" Color="Color.Primary"/> + <MudSwitch @bind-Checked="@response.PlaySetting.IsInverseOn" Label="Inverse" Color="Color.Primary"/> + <MudSwitch @bind-Checked="@response.IsSkipOn" Label="Give Up" Color="Color.Primary"/> + <MudSwitch @bind-Checked="@response.IsVoiceOn" Label="Voice" Color="Color.Primary"/> + </MudStack> + </MudItem> + <MudItem xs="12" md="8"> + <MudStack Spacing="4"> + <MudSelect @bind-Value="@response.PlaySetting.Speed" Label="Speed"> + @for (uint i = 0; i < 15; i++) + { + var index = i; + <MudSelectItem Value="@i">@SpeedStrings[index]</MudSelectItem> + } + </MudSelect> + + <MudSelect @bind-Value="@response.PlaySetting.RandomType" + Label="Random"> + @foreach (var item in Enum.GetValues<RandomType>()) + { + <MudSelectItem Value="@item"/> + } + </MudSelect> + + <MudSelect @bind-Value="@response.ToneId" Label="Tone"> + @for (uint i = 0; i < 19; i++) + { + var index = i; + <MudSelectItem Value="@i">@ToneStrings[index]</MudSelectItem> + } + </MudSelect> + + <MudSlider Class="mb-8" @bind-Value="@response.NotesPosition" Size="Size.Medium" Min="-5" Max="5" Step="1" TickMarks="true" TickMarkLabels="@NotePositionStrings"> + <MudText Typo="Typo.caption">Notes Position</MudText> + </MudSlider> + </MudStack> + </MudItem> + </MudGrid> + </MudStack> + </MudTabPanel> + </MudTabs> + </MudPaper> + </MudItem> + + <MudItem md="4" xs="12" Class="py-4 px-8"> + <MudStack Style="top:100px" Class="sticky"> + <MudButton Disabled="@isSavingOptions" + OnClick="SaveOptions" + Variant="Variant.Filled" + Color="Color.Primary"> + @if (isSavingOptions) + { + <MudProgressCircular Class="ms-n1" Size="Size.Small" Indeterminate="true"/> + <MudText Class="ms-2">Saving...</MudText> + } + else + { + <MudIcon Icon="@Icons.Filled.Save" Class="mx-2"></MudIcon> + <MudText>Save</MudText> + } + </MudButton> + </MudStack> + + </MudItem> </MudGrid> } \ No newline at end of file diff --git a/TaikoWebUI/Pages/Profile.razor.cs b/TaikoWebUI/Pages/Profile.razor.cs index b646c36..31c2d78 100644 --- a/TaikoWebUI/Pages/Profile.razor.cs +++ b/TaikoWebUI/Pages/Profile.razor.cs @@ -1,4 +1,6 @@ -namespace TaikoWebUI.Pages; +using TaikoWebUI.Pages.Dialogs; + +namespace TaikoWebUI.Pages; public partial class Profile { @@ -9,16 +11,31 @@ public partial class Profile private bool isSavingOptions; - private readonly string[] speedStrings = + private static readonly string[] CostumeColors = + { + "#F84828", "#68C0C0", "#DC1500", "#F8F0E0", "#009687", "#00BF87", + "#00FF9A", "#66FFC2", "#FFFFFF", "#690000", "#FF0000", "#FF6666", + "#FFB3B3", "#00BCC2", "#00F7FF", "#66FAFF", "#B3FDFF", "#E4E4E4", + "#993800", "#FF5E00", "#FF9E78", "#FFCFB3", "#005199", "#0088FF", + "#66B8FF", "#B3DBFF", "#B9B9B9", "#B37700", "#FFAA00", "#FFCC66", + "#FFE2B3", "#000C80", "#0019FF", "#6675FF", "#B3BAFF", "#858585", + "#B39B00", "#FFDD00", "#FFFF00", "#FFFF71", "#2B0080", "#5500FF", + "#9966FF", "#CCB3FF", "#505050", "#38A100", "#78C900", "#B3FF00", + "#DCFF8A", "#610080", "#C400FF", "#DC66FF", "#EDB3FF", "#232323", + "#006600", "#00B800", "#00FF00", "#8AFF9E", "#990059", "#FF0095", + "#FF66BF", "#FFB3DF", "#000000" + }; + + private static readonly string[] SpeedStrings = { "1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8", "1.9", "2.0", "2.5", "3.0", "3.5", "4.0" }; - private readonly string[] notePositionStrings = { "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5" }; + private static readonly string[] NotePositionStrings = { "-5", "-4", "-3", "-2", "-1", "0", "+1", "+2", "+3", "+4", "+5" }; - private readonly string[] toneStrings = + private static readonly string[] ToneStrings = { "Taiko", "Festival", "Dogs & Cats", "Deluxe", "Drumset", "Tambourine", "Don Wada", "Clapping", @@ -27,13 +44,13 @@ public partial class Profile "Synth Drum", "Shuriken", "Bubble Pop", "Electric Guitar" }; - private readonly string[] titlePlateStrings = + private static readonly string[] TitlePlateStrings = { "Wood", "Rainbow", "Gold", "Purple", "AI 1", "AI 2", "AI 3", "AI 4" }; - private List<BreadcrumbItem> breadcrumbs = new() + private readonly List<BreadcrumbItem> breadcrumbs = new() { new BreadcrumbItem("Cards", href: "/Cards"), }; @@ -55,4 +72,25 @@ public partial class Profile isSavingOptions = false; } + private async Task OpenChooseTitleDialog() + { + var options = new DialogOptions + { + //CloseButton = false, + CloseOnEscapeKey = false, + DisableBackdropClick = true, + MaxWidth = MaxWidth.Medium, + FullWidth = true + }; + var parameters = new DialogParameters + { + ["UserSetting"] = response + }; + var dialog = DialogService.Show<ChooseTitleDialog>("Player Titles", parameters, options); + var result = await dialog.Result; + if (!result.Cancelled) + { + StateHasChanged(); + } + } } \ No newline at end of file diff --git a/TaikoWebUI/Services/GameDataService.cs b/TaikoWebUI/Services/GameDataService.cs index 4fd7c2f..768832c 100644 --- a/TaikoWebUI/Services/GameDataService.cs +++ b/TaikoWebUI/Services/GameDataService.cs @@ -6,11 +6,19 @@ namespace TaikoWebUI.Services; public class GameDataService : IGameDataService { + private readonly string[] bodyTitles = new string[Constants.COSTUME_BODY_MAX]; private readonly HttpClient client; + private readonly string[] faceTitles = new string[Constants.COSTUME_FACE_MAX]; + + private readonly string[] headTitles = new string[Constants.COSTUME_HEAD_MAX]; + private readonly string[] kigurumiMTitles = new string[Constants.COSTUME_KIGURUMI_MAX]; private readonly Dictionary<uint, MusicDetail> musicMap = new(); + private readonly string[] puchiTitles = new string[Constants.COSTUME_PUCHI_MAX]; - private ImmutableDictionary<uint, DanData> danMap = null!; + private ImmutableDictionary<uint, DanData> danMap = ImmutableDictionary<uint, DanData>.Empty; + + private ImmutableHashSet<Title> titles = ImmutableHashSet<Title>.Empty; public GameDataService(HttpClient client) { @@ -30,15 +38,171 @@ public class GameDataService : IGameDataService danData.ThrowIfNull(); danMap = danData.ToImmutableDictionary(data => data.DanId); - + // To prevent duplicate entries in wordlist var dict = wordList.WordListEntries.GroupBy(entry => entry.Key) .ToImmutableDictionary(group => group.Key, group => group.First()); + await Task.Run(() => InitializeMusicMap(musicInfo, dict, musicOrder)); + + await Task.Run(() => InitializeHeadTitles(dict)); + await Task.Run(() => InitializeFaceTitles(dict)); + await Task.Run(() => InitializeBodyTitles(dict)); + await Task.Run(() => InitializePuchiTitles(dict)); + await Task.Run(() => InitializeKigurumiTitles(dict)); + await Task.Run(() => InitializeTitles(dict)); + } + + public string GetMusicNameBySongId(uint songId) + { + return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty; + } + + public string GetMusicArtistBySongId(uint songId) + { + return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.ArtistName : string.Empty; + } + + public SongGenre GetMusicGenreBySongId(uint songId) + { + return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Genre : SongGenre.Variety; + } + + public int GetMusicIndexBySongId(uint songId) + { + return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Index : int.MaxValue; + } + + public DanData GetDanDataById(uint danId) + { + return danMap.GetValueOrDefault(danId, new DanData()); + } + + public int GetMusicStarLevel(uint songId, Difficulty difficulty) + { + var success = musicMap.TryGetValue(songId, out var musicDetail); + return difficulty switch + { + Difficulty.None => throw new ArgumentException("Difficulty cannot be none"), + Difficulty.Easy => success ? musicDetail!.StarEasy : 0, + Difficulty.Normal => success ? musicDetail!.StarNormal : 0, + Difficulty.Hard => success ? musicDetail!.StarHard : 0, + Difficulty.Oni => success ? musicDetail!.StarOni : 0, + Difficulty.UraOni => success ? musicDetail!.StarUra : 0, + _ => throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null) + }; + } + + public string GetHeadTitle(uint index) + { + return index < headTitles.Length ? headTitles[index] : string.Empty; + } + + public string GetKigurumiTitle(uint index) + { + return index < kigurumiMTitles.Length ? kigurumiMTitles[index] : string.Empty; + } + + public string GetBodyTitle(uint index) + { + return index < bodyTitles.Length ? bodyTitles[index] : string.Empty; + } + + public string GetFaceTitle(uint index) + { + return index < faceTitles.Length ? faceTitles[index] : string.Empty; + } + + public string GetPuchiTitle(uint index) + { + return index < puchiTitles.Length ? puchiTitles[index] : string.Empty; + } + + public ImmutableHashSet<Title> GetTitles() + { + return titles; + } + + private void InitializeTitles(ImmutableDictionary<string, WordListEntry> dict) + { + var set = ImmutableHashSet.CreateBuilder<Title>(); + for (var i = 1; i < Constants.PLAYER_TITLE_MAX; i++) + { + var key = $"syougou_{i}"; + + var titleWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + + set.Add(new Title{ + TitleName = titleWordlistItem.JapaneseText, + TitleId = i + }); + } + + titles = set.ToImmutable(); + } + + private void InitializePuchiTitles(ImmutableDictionary<string, WordListEntry> dict) + { + for (var i = 0; i < Constants.COSTUME_PUCHI_MAX; i++) + { + var key = $"costume_puchi_{i}"; + + var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + puchiTitles[i] = costumeWordlistItem.JapaneseText; + } + } + + private void InitializeKigurumiTitles(ImmutableDictionary<string, WordListEntry> dict) + { + for (var i = 0; i < Constants.COSTUME_KIGURUMI_MAX; i++) + { + var key = $"costume_kigurumi_{i}"; + + var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + kigurumiMTitles[i] = costumeWordlistItem.JapaneseText; + } + } + + private void InitializeBodyTitles(ImmutableDictionary<string, WordListEntry> dict) + { + for (var i = 0; i < Constants.COSTUME_BODY_MAX; i++) + { + var key = $"costume_body_{i}"; + + var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + bodyTitles[i] = costumeWordlistItem.JapaneseText; + } + } + + private void InitializeFaceTitles(ImmutableDictionary<string, WordListEntry> dict) + { + for (var i = 0; i < Constants.COSTUME_FACE_MAX; i++) + { + var key = $"costume_face_{i}"; + + var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + faceTitles[i] = costumeWordlistItem.JapaneseText; + } + } + + private void InitializeHeadTitles(ImmutableDictionary<string, WordListEntry> dict) + { + for (var i = 0; i < Constants.COSTUME_HEAD_MAX; i++) + { + var key = $"costume_head_{i}"; + + var costumeWordlistItem = dict.GetValueOrDefault(key, new WordListEntry()); + headTitles[i] = costumeWordlistItem.JapaneseText; + } + } + + private void InitializeMusicMap(MusicInfo musicInfo, ImmutableDictionary<string, WordListEntry> dict, + MusicOrder musicOrder) + { foreach (var music in musicInfo.Items) { var songNameKey = $"song_{music.Id}"; var songArtistKey = $"song_sub_{music.Id}"; - + var musicName = dict.GetValueOrDefault(songNameKey, new WordListEntry()); var musicArtist = dict.GetValueOrDefault(songArtistKey, new WordListEntry()); @@ -60,42 +224,4 @@ public class GameDataService : IGameDataService } } } - - public string GetMusicNameBySongId(uint songId) - { - return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.SongName : string.Empty; - } - - public string GetMusicArtistBySongId(uint songId) - { - return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.ArtistName : string.Empty; - } - public SongGenre GetMusicGenreBySongId(uint songId) - { - return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Genre : SongGenre.Variety; - } - - public int GetMusicIndexBySongId(uint songId) - { - return musicMap.TryGetValue(songId, out var musicDetail) ? musicDetail.Index : int.MaxValue; - } - public DanData GetDanDataById(uint danId) - { - return danMap.GetValueOrDefault(danId, new DanData()); - } - - public int GetMusicStarLevel(uint songId, Difficulty difficulty) - { - var success = musicMap.TryGetValue(songId, out var musicDetail); - return difficulty switch - { - Difficulty.None => throw new ArgumentException("Difficulty cannot be none"), - Difficulty.Easy => success ? musicDetail!.StarEasy : 0, - Difficulty.Normal => success ? musicDetail!.StarNormal : 0, - Difficulty.Hard => success ? musicDetail!.StarHard : 0, - Difficulty.Oni => success ? musicDetail!.StarOni : 0, - Difficulty.UraOni => success ? musicDetail!.StarUra : 0, - _ => throw new ArgumentOutOfRangeException(nameof(difficulty), difficulty, null) - }; - } } \ No newline at end of file diff --git a/TaikoWebUI/Services/IGameDataService.cs b/TaikoWebUI/Services/IGameDataService.cs index fbad860..b5a7310 100644 --- a/TaikoWebUI/Services/IGameDataService.cs +++ b/TaikoWebUI/Services/IGameDataService.cs @@ -1,4 +1,7 @@ -namespace TaikoWebUI.Services; +using System.Collections.Immutable; +using TaikoWebUI.Shared.Models; + +namespace TaikoWebUI.Services; public interface IGameDataService { @@ -15,4 +18,12 @@ public interface IGameDataService public DanData GetDanDataById(uint danId); public int GetMusicStarLevel(uint songId, Difficulty difficulty); + + public string GetHeadTitle(uint index); + public string GetKigurumiTitle(uint index); + public string GetBodyTitle(uint index); + public string GetFaceTitle(uint index); + public string GetPuchiTitle(uint index); + + public ImmutableHashSet<Title> GetTitles(); } \ No newline at end of file diff --git a/TaikoWebUI/Shared/Constants.cs b/TaikoWebUI/Shared/Constants.cs new file mode 100644 index 0000000..47eb065 --- /dev/null +++ b/TaikoWebUI/Shared/Constants.cs @@ -0,0 +1,12 @@ +namespace TaikoWebUI.Shared; + +public static class Constants +{ + public const int COSTUME_HEAD_MAX = 140; + public const int COSTUME_FACE_MAX = 58; + public const int COSTUME_BODY_MAX = 156; + public const int COSTUME_KIGURUMI_MAX = 154; + public const int COSTUME_PUCHI_MAX = 129; + public const int COSTUME_COLOR_MAX = 63; + public const int PLAYER_TITLE_MAX = 750; +} \ No newline at end of file diff --git a/TaikoWebUI/Shared/Models/Title.cs b/TaikoWebUI/Shared/Models/Title.cs new file mode 100644 index 0000000..d6f68be --- /dev/null +++ b/TaikoWebUI/Shared/Models/Title.cs @@ -0,0 +1,23 @@ +namespace TaikoWebUI.Shared.Models; + +public class Title +{ + public int TitleId { get; set; } + + public string TitleName { get; init; } = string.Empty; + + public override bool Equals(object? obj) + { + if (obj is Title title) + { + return title.TitleName.Equals(TitleName); + } + + return false; + } + + public override int GetHashCode() + { + return TitleName.GetHashCode(); + } +} \ No newline at end of file diff --git a/TaikoWebUI/TaikoWebUI.csproj b/TaikoWebUI/TaikoWebUI.csproj index 02bf122..d6221e1 100644 --- a/TaikoWebUI/TaikoWebUI.csproj +++ b/TaikoWebUI/TaikoWebUI.csproj @@ -7,6 +7,7 @@ </PropertyGroup> <ItemGroup> + <PackageReference Include="Autocomplete.Clients" Version="1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.7" PrivateAssets="all" /> <PackageReference Include="MudBlazor" Version="6.0.15" /> diff --git a/TaikoWebUI/wwwroot/style.overrides.css b/TaikoWebUI/wwwroot/style.overrides.css index 61dab8d..bbae00c 100644 --- a/TaikoWebUI/wwwroot/style.overrides.css +++ b/TaikoWebUI/wwwroot/style.overrides.css @@ -18,4 +18,15 @@ .mud-progress-linear.bar-pass-red .mud-typography { font-weight: bold; color: #333; +} + +.color-box { + width: 16px; + height: 16px; + border-radius: 9999px; + display: inline-block; + margin-right: 10px; + border: 1px solid black; + position: relative; + top: 2px; } \ No newline at end of file