diff --git a/src/Web/AdminPanel/Pages/EditItemDrops.razor b/src/Web/AdminPanel/Pages/EditItemDrops.razor
new file mode 100644
index 000000000..581d6c9c5
--- /dev/null
+++ b/src/Web/AdminPanel/Pages/EditItemDrops.razor
@@ -0,0 +1,98 @@
+@page "/edit-item-drops/"
+
+OpenMU: Item Drop Chances
+
+
+
Item Drop Chances
+
+@if (!this._loadingFinished)
+{
+
+ Loading...
+ return;
+}
+
+ Drop Item Groups
+
+
+
+ | Type |
+ Drop Rate (0.0 to 1.0) |
+ |
+
+
+
+ @if (this._moneyDropGroup is { } moneyDropGroup)
+ {
+
+ | Money Drop: |
+ |
+ |
+
+ }
+ @if (this._randomDropGroup is { } randomDropGroup)
+ {
+
+ | Common Items: |
+ |
+ |
+
+ }
+ @if (this._excellentDropGroup is { } excellentDropGroup)
+ {
+
+ | Excellent Item Drop: |
+ |
+ |
+
+ }
+
+ | No Drop: |
+ @(this.NoDropPercentage.ToString("P")) |
+ |
+
+
+
+
+Options
+
+
+
+ | Type |
+ Add Rate (0.0 to 1.0) |
+ Maximum Options per Item |
+
+
+
+
+ @if (this._luckOption is { } luckOption)
+ {
+
+ | @this._luckOption!.Name: |
+ |
+ |
+
+ }
+ @foreach (var option in this._excellentOptions)
+ {
+
+ | @option.Name: |
+ |
+ |
+
+ }
+ @foreach (var option in this._normalOptions)
+ {
+
+ | @option.Name: |
+ |
+ |
+
+ }
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs b/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs
new file mode 100644
index 000000000..b5bab7c71
--- /dev/null
+++ b/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs
@@ -0,0 +1,199 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.Web.AdminPanel.Pages;
+
+using System.Threading;
+using Blazored.Toast.Services;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Routing;
+using Microsoft.Extensions.Logging;
+using Microsoft.JSInterop;
+using MUnique.OpenMU.DataModel.Configuration;
+using MUnique.OpenMU.DataModel.Configuration.Items;
+using MUnique.OpenMU.Persistence;
+
+///
+/// Implements a simplified page for editing item drops.
+///
+public partial class EditItemDrops : IAsyncDisposable
+{
+ private Task? _loadTask;
+ private CancellationTokenSource? _disposeCts;
+ private IContext? _persistenceContext;
+ private IDisposable? _navigationLockDisposable;
+
+ private DropItemGroup? _randomDropGroup;
+
+ private DropItemGroup? _excellentDropGroup;
+
+ private DropItemGroup? _moneyDropGroup;
+
+ private List _normalOptions = [];
+
+ private List _excellentOptions = [];
+
+ private ItemOptionDefinition? _luckOption;
+
+ private bool _loadingFinished;
+
+ private double NoDropPercentage
+ {
+ get => Math.Max(0, 1 - (this._randomDropGroup!.Chance + this._moneyDropGroup!.Chance + this._excellentDropGroup!.Chance));
+ }
+
+ ///
+ /// Gets or sets the data source.
+ ///
+ [Inject]
+ public IDataSource DataSource { get; set; } = null!;
+
+ ///
+ /// Gets or sets the context provider.
+ ///
+ [Inject]
+ public IPersistenceContextProvider ContextProvider { get; set; } = null!;
+
+ ///
+ /// Gets or sets the toast service.
+ ///
+ [Inject]
+ public IToastService ToastService { get; set; } = null!;
+
+ ///
+ /// Gets or sets the navigation manager.
+ ///
+ [Inject]
+ public NavigationManager NavigationManager { get; set; } = null!;
+
+ ///
+ /// Gets or sets the java script runtime.
+ ///
+ [Inject]
+ public IJSRuntime JavaScript { get; set; } = null!;
+
+ ///
+ /// Gets or sets the logger.
+ ///
+ [Inject]
+ public ILogger Logger { get; set; } = null!;
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ this._navigationLockDisposable?.Dispose();
+ this._navigationLockDisposable = null;
+
+ await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
+ this._disposeCts?.Dispose();
+ this._disposeCts = null;
+
+ try
+ {
+ await (this._loadTask ?? Task.CompletedTask).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ // we can ignore that ...
+ }
+ catch
+ {
+ // and we should not throw exceptions in the dispose method ...
+ }
+ }
+
+ ///
+ protected override Task OnInitializedAsync()
+ {
+ this._navigationLockDisposable = this.NavigationManager.RegisterLocationChangingHandler(this.OnBeforeInternalNavigationAsync);
+ return base.OnInitializedAsync();
+ }
+
+ ///
+ protected override async Task OnParametersSetAsync()
+ {
+ var cts = new CancellationTokenSource();
+ this._disposeCts = cts;
+ this._loadingFinished = false;
+ this._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token), cts.Token);
+ await base.OnParametersSetAsync().ConfigureAwait(true);
+ }
+
+ private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext context)
+ {
+ if (this._persistenceContext?.HasChanges is not true)
+ {
+ return;
+ }
+
+ var isConfirmed = await this.JavaScript.InvokeAsync(
+ "window.confirm",
+ "There are unsaved changes. Are you sure you want to discard them?")
+ .ConfigureAwait(true);
+
+ if (!isConfirmed)
+ {
+ context.PreventNavigation();
+ }
+ else
+ {
+ await this.DataSource.DiscardChangesAsync().ConfigureAwait(true);
+ }
+ }
+
+ private async Task LoadDataAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ this._persistenceContext = await this.DataSource.GetContextAsync(cancellationToken).ConfigureAwait(true);
+ await this.DataSource.GetOwnerAsync(default, cancellationToken).ConfigureAwait(true);
+ cancellationToken.ThrowIfCancellationRequested();
+ var data = this.DataSource.GetAll()
+ .Where(m => m is { Monster: null, })
+ .ToList();
+ this._excellentDropGroup = data.FirstOrDefault(d => d is { ItemType: SpecialItemType.Excellent, PossibleItems.Count: 0 });
+ this._randomDropGroup = data.FirstOrDefault(d => d is { ItemType: SpecialItemType.RandomItem, PossibleItems.Count: 0 });
+ this._moneyDropGroup = data.FirstOrDefault(d => d.ItemType == SpecialItemType.Money);
+
+ var options = this.DataSource.GetAll();
+ this._normalOptions = options.Where(d => d.PossibleOptions.Any(o => o.OptionType == ItemOptionTypes.Option)).ToList();
+ this._excellentOptions = options.Where(d => d.PossibleOptions.Any(o => o.OptionType == ItemOptionTypes.Excellent)).ToList();
+ this._luckOption = options.FirstOrDefault(d => d.PossibleOptions.Any(o => o.OptionType == ItemOptionTypes.Luck));
+
+ this._loadingFinished = true;
+
+ await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(false);
+ }
+
+ private async Task OnSaveButtonClickAsync()
+ {
+ try
+ {
+ if (this._persistenceContext is { } context)
+ {
+ var success = await context.SaveChangesAsync().ConfigureAwait(true);
+ var text = success ? "The changes have been saved." : "There were no changes to save.";
+ this.ToastService.ShowSuccess(text);
+ }
+ else
+ {
+ this.ToastService.ShowError("Failed, context not initialized.");
+ }
+ }
+ catch (Exception ex)
+ {
+ this.Logger.LogError(ex, $"An unexpected error occurred on save: {ex.Message}");
+ this.ToastService.ShowError($"An unexpected error occurred: {ex.Message}");
+ }
+ }
+
+ private async Task OnCancelButtonClickAsync()
+ {
+ if (this._persistenceContext?.HasChanges is true)
+ {
+ await this.DataSource.DiscardChangesAsync().ConfigureAwait(true);
+ await this.LoadDataAsync(this._disposeCts?.Token ?? default).ConfigureAwait(true);
+ }
+ }
+}
diff --git a/src/Web/AdminPanel/Shared/ConfigNavMenu.razor b/src/Web/AdminPanel/Shared/ConfigNavMenu.razor
index 070526bb5..49afb82cb 100644
--- a/src/Web/AdminPanel/Shared/ConfigNavMenu.razor
+++ b/src/Web/AdminPanel/Shared/ConfigNavMenu.razor
@@ -20,6 +20,7 @@
Skills
Items
Drop Item Groups
+ Item Drops (simplified)
Maps
Mini Games
Warp List