diff --git a/PKHeX.Avalonia/Controls/SlotModel.cs b/PKHeX.Avalonia/Controls/SlotModel.cs index 7715ca684..ad682924d 100644 --- a/PKHeX.Avalonia/Controls/SlotModel.cs +++ b/PKHeX.Avalonia/Controls/SlotModel.cs @@ -24,6 +24,20 @@ public partial class SlotModel : ObservableObject [ObservableProperty] private bool _isEmpty = true; + /// Whether the slot matches the current search filter. + [ObservableProperty] + private bool _isHighlighted; + + /// Whether a search filter is currently active. + [ObservableProperty] + private bool _isSearchActive; + + /// Opacity to use when a search is active (1.0 if highlighted or no search, 0.3 if not matching). + public double SearchOpacity => IsHighlighted ? 1.0 : (_isSearchActive ? 0.3 : 1.0); + + partial void OnIsHighlightedChanged(bool value) => OnPropertyChanged(nameof(SearchOpacity)); + partial void OnIsSearchActiveChanged(bool value) => OnPropertyChanged(nameof(SearchOpacity)); + /// The PKM entity backing this slot, if any. [ObservableProperty] private PKM? _entity; @@ -38,6 +52,10 @@ public partial class SlotModel : ObservableObject ? $"{SpeciesName.GetSpeciesNameGeneration(Entity.Species, 2, Entity.Format)} Lv.{Entity.CurrentLevel}" : "Empty"; + /// Label describing the slot type (e.g. "Daycare", "Fused Kyurem"). + [ObservableProperty] + private string _slotLabel = string.Empty; + partial void OnEntityChanged(PKM? value) { OnPropertyChanged(nameof(ShowdownText)); diff --git a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs index a85ca2a89..019b2075f 100644 --- a/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/PKMEditorViewModel.cs @@ -11,6 +11,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PKHeX.Avalonia.Converters; +using PKHeX.Avalonia.ViewModels.Subforms; +using PKHeX.Avalonia.Views.Subforms; using PKHeX.Core; using PKHeX.Drawing.PokeSprite.Avalonia; using SkiaSharp; @@ -165,6 +167,19 @@ public partial class PKMEditorViewModel : ObservableObject [ObservableProperty] private bool _isShadow; [ObservableProperty] private bool _hasShadow; + // Form Argument + [ObservableProperty] private uint _formArgument; + [ObservableProperty] private bool _hasFormArgument; + [ObservableProperty] private uint _formArgumentMax; + + // Alpha Mastered Move (Gen 8a - Legends Arceus) + [ObservableProperty] private ComboItem? _selectedAlphaMove; + [ObservableProperty] private bool _hasAlphaMove; + + // Move Shop / Tech Record visibility + [ObservableProperty] private bool _hasMoveShop; + [ObservableProperty] private bool _hasTechRecords; + // Gen-specific: Catch Rate (Gen 1) [ObservableProperty] private int _catchRate; [ObservableProperty] private bool _hasCatchRate; @@ -950,6 +965,41 @@ public void PopulateFields(PKM pk) PokeStarFame = 0; } + // Form Argument + if (pk is IFormArgument fa) + { + HasFormArgument = true; + FormArgument = fa.FormArgument; + FormArgumentMax = FormArgumentUtil.GetFormArgumentMax(pk.Species, pk.Form, pk.Context); + if (FormArgumentMax == 0) + FormArgumentMax = 255; + } + else + { + HasFormArgument = false; + FormArgument = 0; + FormArgumentMax = 255; + } + + // Alpha Mastered Move (Legends Arceus) + if (pk is PA8 pa8) + { + HasAlphaMove = true; + var alphaMoveId = pa8.AlphaMove; + SelectedAlphaMove = MoveList.FirstOrDefault(m => m.Value == alphaMoveId); + } + else + { + HasAlphaMove = false; + SelectedAlphaMove = null; + } + + // Move Shop (Legends Arceus) + HasMoveShop = pk is IMoveShop8Mastery; + + // Tech Records (Gen 8+) + HasTechRecords = pk is ITechRecord; + // Origin Mark indicator var gen = pk.Generation; HasOriginMark = gen >= 3; @@ -1231,9 +1281,51 @@ public void PopulateFields(PKM pk) pb7Save.Mood = (byte)Math.Clamp(Mood7b, 0, 255); } + // Form Argument + if (Entity is IFormArgument faSave) + faSave.FormArgument = FormArgument; + + // Alpha Mastered Move (Legends Arceus) + if (Entity is PA8 pa8Save && SelectedAlphaMove is not null) + pa8Save.AlphaMove = (ushort)SelectedAlphaMove.Value; + return Entity; } + // --- Move Shop / Tech Record editor commands --- + + [RelayCommand] + private async Task OpenMoveShop() + { + if (Entity is not IMoveShop8Mastery master || Entity is not IMoveShop8 shop) return; + try + { + PreparePKM(); + var vm = new MoveShopEditorViewModel(shop, master, Entity); + var view = new MoveShopEditorView { DataContext = vm }; + var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + if (mainWindow != null) + await view.ShowDialog(mainWindow); + } + catch (Exception ex) { LegalityReport = $"Move Shop error: {ex.Message}"; } + } + + [RelayCommand] + private async Task OpenTechRecords() + { + if (Entity is not ITechRecord tr) return; + try + { + PreparePKM(); + var vm = new TechRecordEditorViewModel(tr, Entity); + var view = new TechRecordEditorView { DataContext = vm }; + var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow; + if (mainWindow != null) + await view.ShowDialog(mainWindow); + } + catch (Exception ex) { LegalityReport = $"Tech Record error: {ex.Message}"; } + } + // --- IV/EV quick-set commands --- [RelayCommand] @@ -1402,6 +1494,46 @@ private string GetNatureColor(int statIndex) }; } + // --- Nickname warning --- + + [RelayCommand] + private void ShowNicknameWarning() + { + if (Entity is null) return; + var nickname = Nickname ?? string.Empty; + var species = Entity.Species; + var lang = Entity.Language; + var defaultName = SpeciesName.GetSpeciesNameGeneration(species, lang, Entity.Format); + + // Check for non-ASCII characters that might not render in older games + bool hasSpecialChars = false; + foreach (var c in nickname) + { + if (c > 0x7E || c < 0x20) + { + hasSpecialChars = true; + break; + } + } + + if (string.IsNullOrEmpty(nickname)) + { + LegalityReport = "Nickname is empty."; + } + else if (nickname == defaultName) + { + LegalityReport = $"Nickname matches default species name: '{defaultName}'."; + } + else if (hasSpecialChars) + { + LegalityReport = $"Nickname '{nickname}' contains special characters that may not render correctly in-game. Default name: '{defaultName}'."; + } + else + { + LegalityReport = $"Nickname '{nickname}' appears to use standard characters. Default name: '{defaultName}'."; + } + } + // --- Sprite context menu commands --- [RelayCommand] diff --git a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs index 1633e64a0..4bae5f4e0 100644 --- a/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs +++ b/PKHeX.Avalonia/ViewModels/SAVEditorViewModel.cs @@ -127,6 +127,18 @@ public void Redo() [ObservableProperty] private Bitmap? _boxWallpaper; + // Box search/filter + [ObservableProperty] private string _searchText = string.Empty; + [ObservableProperty] private string _searchResultText = string.Empty; + + // Extra slots (Other tab) + public ObservableCollection ExtraSlots { get; } = []; + [ObservableProperty] private bool _hasExtraSlots; + + // Save slot info + [ObservableProperty] private string _saveSlotInfo = string.Empty; + [ObservableProperty] private bool _hasSaveSlotInfo; + public ObservableCollection BoxSlots { get; } = []; public ObservableCollection PartySlots { get; } = []; public ObservableCollection BoxNames { get; } = []; @@ -181,6 +193,9 @@ public void LoadSaveFile(SaveFile sav) RefreshParty(); UpdateToolVisibility(sav); RefreshDaycare(sav); + RefreshExtraSlots(sav); + RefreshSaveSlotInfo(sav); + SearchText = string.Empty; } private void UpdateToolVisibility(SaveFile sav) @@ -455,6 +470,125 @@ private void RefreshDaycare(SaveFile sav) } } + #region Box Search + + partial void OnSearchTextChanged(string value) + { + if (_sav is null) return; + var searchLower = value.ToLowerInvariant().Trim(); + var isActive = !string.IsNullOrEmpty(searchLower); + int count = 0; + + foreach (var slot in BoxSlots) + { + slot.IsSearchActive = isActive; + if (slot.Entity is null || slot.Entity.Species == 0) + { + slot.IsHighlighted = false; + continue; + } + if (!isActive) + { + slot.IsHighlighted = false; + continue; + } + var name = SpeciesName.GetSpeciesNameGeneration(slot.Entity.Species, 2, slot.Entity.Format).ToLowerInvariant(); + var matches = name.Contains(searchLower); + slot.IsHighlighted = matches; + if (matches) count++; + } + + SearchResultText = isActive ? $"{count} found" : string.Empty; + } + + #endregion + + #region Extra Slots + + private void RefreshExtraSlots(SaveFile sav) + { + ExtraSlots.Clear(); + try + { + var extras = sav.GetExtraSlots(true); + if (extras.Count == 0) + { + HasExtraSlots = false; + return; + } + + HasExtraSlots = true; + foreach (var slotInfo in extras) + { + var pk = slotInfo.Read(sav); + var model = new SlotModel + { + Slot = slotInfo.Slot, + SlotLabel = GetSlotTypeLabel(slotInfo.Type, slotInfo.Slot), + }; + model.Entity = pk; + if (pk.Species == 0) + { + model.SetImage(SpriteUtil.Spriter.None); + model.IsEmpty = true; + } + else + { + var sprite = pk.Sprite(); + model.SetImage(sprite); + model.IsEmpty = false; + } + ExtraSlots.Add(model); + } + } + catch + { + HasExtraSlots = false; + } + } + + private static string GetSlotTypeLabel(StorageSlotType type, int slot) => type switch + { + StorageSlotType.Daycare => $"Daycare #{slot + 1}", + StorageSlotType.GTS => "GTS Upload", + StorageSlotType.BattleBox => $"Battle Box #{slot + 1}", + StorageSlotType.FusedKyurem => "Fused Kyurem", + StorageSlotType.FusedNecrozmaS => "Fused Necrozma (Solgaleo)", + StorageSlotType.FusedNecrozmaM => "Fused Necrozma (Lunala)", + StorageSlotType.FusedCalyrex => "Fused Calyrex", + StorageSlotType.Resort => $"Resort #{slot + 1}", + StorageSlotType.Ride => "Ride Legend", + StorageSlotType.BattleAgency => $"Battle Agency #{slot + 1}", + StorageSlotType.SurpriseTrade => $"Surprise Trade #{slot + 1}", + StorageSlotType.Underground => $"Underground #{slot + 1}", + StorageSlotType.Scripted => $"Scripted #{slot + 1}", + StorageSlotType.PGL => "PGL Upload", + StorageSlotType.Pokéwalker => "Pokewalker", + StorageSlotType.Shiny => $"Shiny Cache #{slot + 1}", + _ => $"{type} #{slot + 1}", + }; + + #endregion + + #region Save Slot Info + + private void RefreshSaveSlotInfo(SaveFile sav) + { + var sb = new StringBuilder(); + sb.AppendLine($"Type: {sav.GetType().Name}"); + sb.AppendLine($"Generation: {sav.Generation}"); + sb.AppendLine($"Version: {sav.Version}"); + sb.AppendLine($"Boxes: {sav.BoxCount} ({sav.BoxSlotCount} slots each)"); + if (sav.HasParty) + sb.AppendLine($"Party: {sav.PartyCount} Pokemon"); + sb.AppendLine($"Checksums: {(sav.ChecksumsValid ? "Valid" : "Invalid")}"); + + SaveSlotInfo = sb.ToString().TrimEnd(); + HasSaveSlotInfo = true; + } + + #endregion + #region Box Management (Sort / Clear) [RelayCommand] diff --git a/PKHeX.Avalonia/Views/PKMEditorView.axaml b/PKHeX.Avalonia/Views/PKMEditorView.axaml index 266f29c55..2711a5984 100644 --- a/PKHeX.Avalonia/Views/PKMEditorView.axaml +++ b/PKHeX.Avalonia/Views/PKMEditorView.axaml @@ -69,7 +69,12 @@ - + + +