diff --git a/src/Web/AdminPanel/Components/ItemDrops/CollapsibleSection.razor b/src/Web/AdminPanel/Components/ItemDrops/CollapsibleSection.razor new file mode 100644 index 000000000..1eb89d09f --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/CollapsibleSection.razor @@ -0,0 +1,72 @@ +
+
+ +
@this.Title
+ @if (this.Badge is not null) + { + @this.Badge + } +
+ @if (this._isExpanded) + { +
+ @if (!string.IsNullOrWhiteSpace(this.Description)) + { +

@this.Description

+ } + @this.ChildContent +
+ } +
+ +@code { + private bool _isExpanded; + private bool _initialized; + + /// + /// Gets or sets the title. + /// + [Parameter] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the description which is shown above the content. + /// + [Parameter] + public string? Description { get; set; } + + /// + /// Gets or sets the badge text which is shown in the header, e.g. the number of entries. + /// + [Parameter] + public string? Badge { get; set; } + + /// + /// Gets or sets a value indicating whether the section is expanded initially. + /// + [Parameter] + public bool InitiallyExpanded { get; set; } + + /// + /// Gets or sets the content. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + protected override void OnParametersSet() + { + base.OnParametersSet(); + if (!this._initialized) + { + this._isExpanded = this.InitiallyExpanded; + this._initialized = true; + } + } + + private void Toggle() + { + this._isExpanded = !this._isExpanded; + } +} diff --git a/src/Web/AdminPanel/Components/ItemDrops/CollectionListAdapter.cs b/src/Web/AdminPanel/Components/ItemDrops/CollectionListAdapter.cs new file mode 100644 index 000000000..b88af5234 --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/CollectionListAdapter.cs @@ -0,0 +1,90 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.AdminPanel.Components.ItemDrops; + +using System.Collections; + +/// +/// Adapts a to a , so that it can be used +/// with components which require a list, e.g. the . +/// All modifications are passed through to the underlying collection. +/// +/// The type of the elements. +public sealed class CollectionListAdapter : IList +{ + private readonly ICollection _collection; + + /// + /// Initializes a new instance of the class. + /// + /// The adapted collection. + public CollectionListAdapter(ICollection collection) + { + this._collection = collection; + } + + /// + public int Count => this._collection.Count; + + /// + public bool IsReadOnly => this._collection.IsReadOnly; + + /// + public T this[int index] + { + get => this._collection.ElementAt(index); + set => throw new NotSupportedException(); + } + + /// + public IEnumerator GetEnumerator() => this._collection.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + public void Add(T item) + { + if (!this._collection.Contains(item)) + { + this._collection.Add(item); + } + } + + /// + public void Clear() => this._collection.Clear(); + + /// + public bool Contains(T item) => this._collection.Contains(item); + + /// + public void CopyTo(T[] array, int arrayIndex) => this._collection.CopyTo(array, arrayIndex); + + /// + public bool Remove(T item) => this._collection.Remove(item); + + /// + public int IndexOf(T item) + { + var index = 0; + foreach (var element in this._collection) + { + if (Equals(element, item)) + { + return index; + } + + index++; + } + + return -1; + } + + /// + public void Insert(int index, T item) => this.Add(item); + + /// + public void RemoveAt(int index) => this._collection.Remove(this[index]); +} diff --git a/src/Web/AdminPanel/Components/ItemDrops/DropGroupAssignmentList.cs b/src/Web/AdminPanel/Components/ItemDrops/DropGroupAssignmentList.cs new file mode 100644 index 000000000..780e5e19e --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/DropGroupAssignmentList.cs @@ -0,0 +1,102 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.AdminPanel.Components.ItemDrops; + +using System.Collections; +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// A list of owners (e.g. maps or monsters) to which a is assigned. +/// Adding or removing an owner adds or removes the drop item group to or from the +/// drop item groups of the owner. +/// +/// The type of the owner. +public sealed class DropGroupAssignmentList : IList + where TOwner : class +{ + private readonly DropItemGroup _group; + private readonly Func> _groupsOfOwner; + private readonly List _owners; + + /// + /// Initializes a new instance of the class. + /// + /// The drop item group. + /// All possible owners. + /// The function to get the drop item groups of an owner. + public DropGroupAssignmentList(DropItemGroup group, IEnumerable allOwners, Func> groupsOfOwner) + { + this._group = group; + this._groupsOfOwner = groupsOfOwner; + this._owners = allOwners.Where(owner => groupsOfOwner(owner).Contains(group)).ToList(); + } + + /// + public int Count => this._owners.Count; + + /// + public bool IsReadOnly => false; + + /// + public TOwner this[int index] + { + get => this._owners[index]; + set => throw new NotSupportedException(); + } + + /// + public IEnumerator GetEnumerator() => this._owners.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + public void Add(TOwner item) + { + if (this._owners.Contains(item)) + { + return; + } + + var groups = this._groupsOfOwner(item); + if (!groups.Contains(this._group)) + { + groups.Add(this._group); + } + + this._owners.Add(item); + } + + /// + public void Clear() + { + foreach (var owner in this._owners.ToList()) + { + this.Remove(owner); + } + } + + /// + public bool Contains(TOwner item) => this._owners.Contains(item); + + /// + public void CopyTo(TOwner[] array, int arrayIndex) => this._owners.CopyTo(array, arrayIndex); + + /// + public bool Remove(TOwner item) + { + this._groupsOfOwner(item).Remove(this._group); + return this._owners.Remove(item); + } + + /// + public int IndexOf(TOwner item) => this._owners.IndexOf(item); + + /// + public void Insert(int index, TOwner item) => this.Add(item); + + /// + public void RemoveAt(int index) => this.Remove(this._owners[index]); +} diff --git a/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor new file mode 100644 index 000000000..26f351ac3 --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor @@ -0,0 +1,152 @@ +@using MUnique.OpenMU.DataModel.Configuration +@using MUnique.OpenMU.DataModel.Configuration.Items +@using MUnique.OpenMU.Persistence +@using MUnique.OpenMU.Web.AdminPanel.Properties + +@if (this.Groups.Count == 0) +{ +

@Resources.NoDropItemGroups

+ return; +} + +
+ + + + + + + @if (this.IsItemBoxDrop) + { + + + + + + + } + else + { + + + + } + @if (this.UsageProvider is not null) + { + + } + + + + + @foreach (var group in this.Groups) + { + var isExpanded = this._expandedGroups.Contains(group); + var boxGroup = group as ItemDropItemGroup; + + + + + @if (this.IsItemBoxDrop && boxGroup is not null) + { + + + + + + + } + else + { + + + + } + @if (this.UsageProvider is not null) + { + + } + + + + + + @if (isExpanded) + { + var possibleItems = this.GetPossibleItems(group); + var assignedMaps = this.Maps is null ? null : this.GetAssignedMaps(group); + + + + } + } + +
@Resources.Description@Resources.ItemType@Resources.ChancePercent@Resources.SourceItemLevel@Resources.MinimumItemLevel@Resources.MaximumItemLevel@Resources.MoneyAmount@Resources.RequiredCharacterLevel@Resources.DropEffect@Resources.ItemLevel@Resources.MinimumMonsterLevel@Resources.MaximumMonsterLevel@Resources.UsedBy
+ + + + + + + + @this.UsageProvider(group) + + @if (this.OnRemove.HasDelegate) + { + + } +
+ @Resources.PossibleItems: @GetItemsSummary(group) +
+ +
+
+ + + @Resources.PossibleItemsHint +
+ @if (assignedMaps is not null) + { +
+ + + + +
+ } + @if (this.ShowMonsterRestriction && !this.IsItemBoxDrop) + { +
+ + +
+ } +
+
+
+
diff --git a/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.cs b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.cs new file mode 100644 index 000000000..6178ee9b7 --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.cs @@ -0,0 +1,164 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Web.AdminPanel.Components.ItemDrops; + +using System.Globalization; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.Interfaces; +using MUnique.OpenMU.Web.AdminPanel.Properties; + +/// +/// A compact, inline editable table of s. +/// +public partial class DropItemGroupTable +{ + private static readonly CultureInfo NeutralCulture = CultureInfo.GetCultureInfo(LocalizedString.NeutralLanguageCode); + + private readonly HashSet _expandedGroups = []; + private readonly Dictionary _editContexts = []; + private readonly Dictionary> _possibleItems = []; + private readonly Dictionary> _assignedMaps = []; + + /// + /// Gets or sets the drop item groups which should be shown. + /// + [Parameter] + public IReadOnlyList Groups { get; set; } = []; + + /// + /// Gets or sets a value indicating whether the groups are s + /// which are dropped by items, e.g. boxes. + /// + [Parameter] + public bool IsItemBoxDrop { get; set; } + + /// + /// Gets or sets all available maps. If set, the maps to which a group is assigned can be edited. + /// + [Parameter] + public IEnumerable? Maps { get; set; } + + /// + /// Gets or sets a value indicating whether the restriction can be edited. + /// + [Parameter] + public bool ShowMonsterRestriction { get; set; } + + /// + /// Gets or sets the function which provides a text which describes where a group is used. + /// If it's , the column is not shown. + /// + [Parameter] + public Func? UsageProvider { get; set; } + + /// + /// Gets or sets the callback which is called when the remove button of a group has been clicked. + /// If it has no delegate, no remove button is shown. + /// + [Parameter] + public EventCallback OnRemove { get; set; } + + /// + /// Gets or sets the title of the remove button. + /// + [Parameter] + public string? RemoveTitle { get; set; } + + /// + /// Gets or sets the callback which is called when the map assignment of a group changed. + /// + [Parameter] + public EventCallback OnMapsChanged { get; set; } + + private int ColumnCount => (this.IsItemBoxDrop ? 10 : 7) + (this.UsageProvider is null ? 0 : 1); + + private static double ToPercent(double chance) => Math.Round(chance * 100.0, 8); + + private static double FromPercent(double percent) => Math.Clamp(percent / 100.0, 0.0, 1.0); + + private static void SetDescription(DropItemGroup group, string? text) + { + group.Description = group.Description.WithTranslation(NeutralCulture, text); + } + + private static string GetItemsSummary(DropItemGroup group) + { + if (group.PossibleItems.Count == 0) + { + return group.ItemType switch + { + SpecialItemType.Money => Resources.Money, + SpecialItemType.RandomItem or SpecialItemType.Excellent or SpecialItemType.SocketItem => Resources.RandomItemsByMonsterLevel, + SpecialItemType.Ancient => Resources.RandomAncientItems, + _ => Resources.NoItems, + }; + } + + return string.Join(", ", group.PossibleItems + .Select(item => item.GetNameForLevel(group.ItemLevel ?? 0)) + .Order(StringComparer.CurrentCultureIgnoreCase)); + } + + private void ToggleExpanded(DropItemGroup group) + { + if (!this._expandedGroups.Remove(group)) + { + this._expandedGroups.Add(group); + } + } + + private EditContext GetEditContext(DropItemGroup group) + { + if (!this._editContexts.TryGetValue(group, out var editContext)) + { + editContext = new EditContext(group); + this._editContexts.Add(group, editContext); + } + + return editContext; + } + + private IList GetPossibleItems(DropItemGroup group) + { + if (!this._possibleItems.TryGetValue(group, out var list)) + { + list = new CollectionListAdapter(group.PossibleItems); + this._possibleItems.Add(group, list); + } + + return list; + } + + private IList GetAssignedMaps(DropItemGroup group) + { + if (!this._assignedMaps.TryGetValue(group, out var list)) + { + list = new DropGroupAssignmentList(group, this.Maps ?? [], map => map.DropItemGroups); + this._assignedMaps.Add(group, list); + } + + return list; + } + + private async Task AssignToAllMapsAsync(DropItemGroup group) + { + var list = this.GetAssignedMaps(group); + foreach (var map in this.Maps ?? []) + { + list.Add(map); + } + + await this.OnMapsChanged.InvokeAsync().ConfigureAwait(true); + } + + private async Task RemoveFromAllMapsAsync(DropItemGroup group) + { + this.GetAssignedMaps(group).Clear(); + await this.OnMapsChanged.InvokeAsync().ConfigureAwait(true); + } +} diff --git a/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.css b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.css new file mode 100644 index 000000000..925610e66 --- /dev/null +++ b/src/Web/AdminPanel/Components/ItemDrops/DropItemGroupTable.razor.css @@ -0,0 +1,30 @@ +.drop-group-table .description-column { + min-width: 14rem; +} + +.drop-group-table .number-input { + width: 4.5rem; +} + +.drop-group-table .money-input { + width: 7rem; +} + +.drop-group-table .group-row > td { + border-bottom-width: 0; +} + +.drop-group-table .items-row > td { + padding-top: 0; + font-size: 0.875em; + white-space: normal; +} + +.drop-group-table .details-row > td { + padding: 0.75rem 1rem; + border-left: 3px solid var(--bs-primary, #0d6efd); +} + +.drop-group-table .type-select { + min-width: 9rem; +} diff --git a/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor b/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor index 492ce0c87..22d8685af 100644 --- a/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor +++ b/src/Web/AdminPanel/Components/Layout/ConfigNavMenu.razor @@ -17,6 +17,7 @@ @Resources.Skills @Resources.Items @Resources.DropItemGroups + @Resources.ItemDrops @Resources.GameMaps @Resources.MiniGames @Resources.WarpList diff --git a/src/Web/AdminPanel/Pages/EditItemDrops.razor b/src/Web/AdminPanel/Pages/EditItemDrops.razor new file mode 100644 index 000000000..df608fd2d --- /dev/null +++ b/src/Web/AdminPanel/Pages/EditItemDrops.razor @@ -0,0 +1,243 @@ +@page "/edit-item-drops" + +@using MUnique.OpenMU.DataModel.Configuration +@using MUnique.OpenMU.DataModel.Configuration.Items +@using MUnique.OpenMU.Persistence +@using MUnique.OpenMU.Web.AdminPanel.Components.ItemDrops +@using MUnique.OpenMU.Web.AdminPanel.Properties + +OpenMU: @Resources.ItemDrops + + +

@Resources.ItemDrops

+

@Resources.ItemDropsPageDescription

+ +@if (this._gameConfiguration is not { } gameConfiguration || this._persistenceContext is null) +{ + return; +} + + + + +
+
+ +
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + + + + + + + + + + + + @foreach (var (map, overviews) in this.MonsterDropOverviewsPerMap) + { + + + + + @foreach (var overview in overviews) + { + + + + + + + + + + } + + } +
@Resources.Monster@Resources.MonsterLevel@Resources.NumberOfMaximumItemDrops@Resources.DropItemGroups@Resources.GuaranteedDrops@Resources.TotalChance@Resources.NoDropChance
@map.Number - @map.Name
@overview.Monster.Number - @overview.Monster.Designation@overview.Level@overview.Monster.NumberOfMaximumItemDrops@overview.Groups.Count@overview.GuaranteedDrops@overview.TotalChance.ToString("P2")@overview.NoDropChance.ToString("P2")
+
+
+ + + + + + + + @foreach (var monster in this.FilteredMonsters) + { +
+
+
@monster.Number - @monster.Designation
+ +
+ + +
+ } + +
+
+ +
+ +
+
+
+ + + + + + + @foreach (var boxItem in this.FilteredBoxItems) + { +
+
@boxItem.Name (@boxItem.Group, @boxItem.Number)
+ + +
+ } + +
+
+ +
+ +
+
+
+ + i.DropsFromMonsters)} / {gameConfiguration.Items.Count}")"> +
+ + +
+
+ + + + + + + + + + + @foreach (var item in this.FilteredItems) + { + + + + + + + } + +
@Resources.Item@Resources.DropsFromMonsters@Resources.DropLevel@Resources.MaximumDropLevel
@item.Name (@item.Group, @item.Number)
+
+
+ + +
+ + + + + + + + + + + @foreach (var option in this.FilteredOptions) + { + + + + + + + } + +
@Resources.ItemOption@Resources.AddsRandomly@Resources.AddChancePercent@Resources.MaximumOptionsPerItem
@option.Name + +
+
+
+ +
+ + +
+ +
+
diff --git a/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs b/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs new file mode 100644 index 000000000..92ea9f7b2 --- /dev/null +++ b/src/Web/AdminPanel/Pages/EditItemDrops.razor.cs @@ -0,0 +1,600 @@ +// +// 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 Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +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.GameLogic.Attributes; +using MUnique.OpenMU.Persistence; +using MUnique.OpenMU.Web.AdminPanel.Properties; +using MUnique.OpenMU.Web.Shared; +using MUnique.OpenMU.Web.Shared.Components.Modal; +using MUnique.OpenMU.Web.Shared.Components.Toast; +using MUnique.OpenMU.Web.Shared.Services; + +/// +/// Page which allows to configure all item drops of the game configuration on one page. +/// +public partial class EditItemDrops : ComponentBase, IAsyncDisposable +{ + private readonly AdditionSelection _addition = new(); + private readonly EditContext _additionEditContext; + + private Task? _loadTask; + private CancellationTokenSource? _disposeCts; + private IContext? _persistenceContext; + private IDisposable? _navigationLockDisposable; + + private GameConfiguration? _gameConfiguration; + private List _maps = []; + private List _monsters = []; + private List _items = []; + private List _options = []; + private Dictionary _usages = []; + + private string _filter = string.Empty; + private bool _showOnlyDroppableItems; + + /// + /// Initializes a new instance of the class. + /// + public EditItemDrops() + { + this._additionEditContext = new EditContext(this._addition); + } + + /// + /// Gets or sets the data source. + /// + [Inject] + public IDataSource DataSource { get; set; } = null!; + + /// + /// Gets or sets the toast service. + /// + [Inject] + public IToastService ToastService { get; set; } = null!; + + /// + /// Gets or sets the modal service. + /// + [Inject] + public IModalService ModalService { 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!; + + /// + /// Gets or sets the loading overlay service. + /// + [Inject] + public LoadingOverlayService LoadingService { get; set; } = null!; + + private bool HasFilter => !string.IsNullOrWhiteSpace(this._filter); + + /// + /// Gets the groups which are assigned to maps (or not assigned at all), but not to monsters, quests or mini games. + /// + private IReadOnlyList MapGroups + { + get + { + var monsterGroups = this._monsters.SelectMany(m => m.DropItemGroups).ToHashSet(); + return this._gameConfiguration!.DropItemGroups + .Where(g => g is not ItemDropItemGroup && !monsterGroups.Contains(g) && !this._usages.ContainsKey(g)) + .Where(this.IsMatchingFilter) + .OrderByDescending(g => g.PossibleItems.Count == 0) + .ThenByDescending(g => g.Chance) + .ThenBy(g => g.Description.ValueInNeutralLanguage) + .ToList(); + } + } + + /// + /// Gets the groups which are used by quests or mini games, but are not assigned to monsters. + /// + private IReadOnlyList QuestAndEventGroups + { + get + { + var monsterGroups = this._monsters.SelectMany(m => m.DropItemGroups).ToHashSet(); + return this._usages.Keys + .Where(g => !monsterGroups.Contains(g)) + .Where(this.IsMatchingFilter) + .OrderBy(g => this._usages[g]) + .ThenBy(g => g.Description.ValueInNeutralLanguage) + .ToList(); + } + } + + /// + /// Gets the drop overviews of the monsters which are spawned on each map, filtered by the current filter. + /// + private IEnumerable<(GameMapDefinition Map, IReadOnlyList Monsters)> MonsterDropOverviewsPerMap + { + get + { + foreach (var map in this._maps) + { + var mapMatches = !this.HasFilter || this.Matches(map.Name); + var overviews = map.MonsterSpawns + .Select(spawn => spawn.MonsterDefinition) + .OfType() + .Where(monster => monster.ObjectKind is NpcObjectKind.Monster or NpcObjectKind.Destructible) + .Distinct() + .Select(monster => new MonsterDropOverview(monster, map)) + .Where(overview => mapMatches + || this.Matches(overview.Monster.Designation) + || overview.Groups.Any(this.IsMatchingFilter)) + .OrderBy(overview => overview.Level) + .ThenBy(overview => overview.Monster.Number) + .ToList(); + if (overviews.Count > 0) + { + yield return (map, overviews); + } + } + } + } + + private IEnumerable FilteredMonsters => + this._monsters + .Where(m => m.DropItemGroups.Count > 0) + .Where(m => !this.HasFilter + || this.Matches(m.Designation) + || m.DropItemGroups.Any(this.IsMatchingFilter)); + + private IEnumerable FilteredBoxItems => + this._items + .Where(i => i.DropItems.Count > 0) + .Where(i => !this.HasFilter + || this.Matches(i.Name) + || i.DropItems.Any(this.IsMatchingFilter)); + + private IEnumerable FilteredItems => + this._items + .Where(i => !this._showOnlyDroppableItems || i.DropsFromMonsters) + .Where(i => !this.HasFilter || this.Matches(i.Name)); + + private IEnumerable FilteredOptions => + this._options.Where(o => !this.HasFilter || this.Matches(o.Name)); + + /// + 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._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token)); + await base.OnParametersSetAsync().ConfigureAwait(true); + } + + private static double ToPercent(float chance) => Math.Round(chance * 100.0, 6); + + private static float FromPercent(double percent) => (float)Math.Clamp(percent / 100.0, 0.0, 1.0); + + private static bool IsRelevantOption(ItemOptionDefinition option) + { + return option.AddsRandomly + || option.PossibleOptions.Any(o => o.OptionType == ItemOptionTypes.Luck + || o.OptionType == ItemOptionTypes.Option + || o.OptionType == ItemOptionTypes.Excellent); + } + + private bool Matches(string? text) + { + return text?.Contains(this._filter.Trim(), StringComparison.OrdinalIgnoreCase) is true; + } + + private bool IsMatchingFilter(DropItemGroup group) + { + return !this.HasFilter + || this.Matches(group.Description.ToString()) + || this.Matches(group.ItemType.ToString()) + || this.Matches(group.Monster?.Designation) + || (this._usages.TryGetValue(group, out var usage) && this.Matches(usage)) + || group.PossibleItems.Any(i => this.Matches(i.Name)); + } + + private IReadOnlyList GetMonsterGroups(MonsterDefinition monster) + { + return monster.DropItemGroups + .OrderByDescending(g => g.Chance) + .ThenBy(g => g.Description.ValueInNeutralLanguage) + .ToList(); + } + + private IReadOnlyList GetBoxGroups(ItemDefinition item) + { + return item.DropItems + .OrderBy(g => g.SourceItemLevel) + .ThenByDescending(g => g.Chance) + .ToList(); + } + + private string? GetUsage(DropItemGroup group) + { + return this._usages.GetValueOrDefault(group); + } + + private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext context) + { + if (this._persistenceContext?.HasChanges is not true) + { + return; + } + + var isConfirmed = await this.JavaScript.InvokeAsync( + "window.confirm", + Resources.UnsavedChangesQuestion) + .ConfigureAwait(true); + + if (!isConfirmed) + { + context.PreventNavigation(); + } + else + { + await this.DataSource.DiscardChangesAsync().ConfigureAwait(true); + } + } + + private async Task LoadDataAsync(CancellationToken cancellationToken) + { + using var loading = this.LoadingService.ShowLoadingIndicator(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + + this._persistenceContext = await this.DataSource.GetContextAsync(cancellationToken).ConfigureAwait(true); + var gameConfiguration = await this.DataSource.GetOwnerAsync(cancellationToken: cancellationToken).ConfigureAwait(true); + cancellationToken.ThrowIfCancellationRequested(); + + this._maps = gameConfiguration.Maps.OrderBy(m => m.Number).ToList(); + this._monsters = gameConfiguration.Monsters.OrderBy(m => m.Number).ToList(); + this._items = gameConfiguration.Items.OrderBy(i => i.Group).ThenBy(i => i.Number).ToList(); + this._options = gameConfiguration.ItemOptions + .Where(IsRelevantOption) + .OrderBy(o => o.Name.ValueInNeutralLanguage) + .ToList(); + this._usages = this.DetermineUsages(gameConfiguration); + this._gameConfiguration = gameConfiguration; + + await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(true); + } + catch (OperationCanceledException) + { + // Expected when navigating away - ignore + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Error loading item drop data"); + } + } + + /// + /// Determines where drop item groups are used, apart from maps, monsters and items. + /// + /// The game configuration. + /// A text for each group which describes the usage. + private Dictionary DetermineUsages(GameConfiguration gameConfiguration) + { + var usages = new Dictionary>(); + void AddUsage(DropItemGroup? group, string usage) + { + if (group is null) + { + return; + } + + if (!usages.TryGetValue(group, out var list)) + { + list = []; + usages.Add(group, list); + } + + if (!list.Contains(usage)) + { + list.Add(usage); + } + } + + foreach (var quest in gameConfiguration.Monsters.SelectMany(m => m.Quests)) + { + foreach (var requirement in quest.RequiredItems) + { + AddUsage(requirement.DropItemGroup, string.Format(Resources.QuestUsage, quest.Name)); + } + } + + foreach (var miniGame in gameConfiguration.MiniGameDefinitions) + { + foreach (var reward in miniGame.Rewards) + { + AddUsage(reward.ItemReward, string.Format(Resources.MiniGameUsage, miniGame.Name)); + } + } + + return usages.ToDictionary(pair => pair.Key, pair => string.Join(", ", pair.Value)); + } + + private DropItemGroup CreateGroup() + { + var group = this._persistenceContext!.CreateNew(); + group.Description = Resources.NewDropItemGroup; + group.Chance = 0.01; + group.ItemType = SpecialItemType.None; + this._gameConfiguration!.DropItemGroups.Add(group); + return group; + } + + private void OnAddMapGroup() + { + this.CreateGroup(); + } + + private void OnAddMonsterGroup(MonsterDefinition monster) + { + var group = this.CreateGroup(); + group.Description = string.Format(Resources.NewDropItemGroupOf, monster.Designation); + monster.DropItemGroups.Add(group); + } + + private void OnAddGroupForSelectedMonster() + { + if (this._addition.Monster is { } monster) + { + this.OnAddMonsterGroup(monster); + this._addition.Monster = null; + } + } + + private void OnAddBoxGroup(ItemDefinition item) + { + var group = this._persistenceContext!.CreateNew(); + group.Description = string.Format(Resources.NewDropItemGroupOf, item.Name); + group.Chance = 0.1; + group.ItemType = SpecialItemType.None; + item.DropItems.Add(group); + } + + private void OnAddGroupForSelectedItem() + { + if (this._addition.Item is { } item) + { + this.OnAddBoxGroup(item); + this._addition.Item = null; + } + } + + private async Task OnDeleteGroupAsync(DropItemGroup group) + { + if (this._usages.TryGetValue(group, out var usage)) + { + this.ToastService.ShowError(string.Format(Resources.DropItemGroupStillUsed, group.Description, usage)); + return; + } + + var isConfirmed = await this.ModalService.ShowQuestionAsync( + Resources.AreYouSure, + string.Format(Resources.DeleteDropItemGroupQuestion, group.Description)) + .ConfigureAwait(true); + if (!isConfirmed) + { + return; + } + + await this.DeleteGroupAsync(group).ConfigureAwait(true); + } + + private async Task OnRemoveGroupFromMonsterAsync(MonsterDefinition monster, DropItemGroup group) + { + monster.DropItemGroups.Remove(group); + + var isStillUsed = this._usages.ContainsKey(group) + || this._monsters.Any(m => m.DropItemGroups.Contains(group)) + || this._maps.Any(m => m.DropItemGroups.Contains(group)); + if (!isStillUsed) + { + await this.DeleteGroupAsync(group).ConfigureAwait(true); + } + } + + private async Task OnRemoveBoxGroupAsync(ItemDefinition item, DropItemGroup group) + { + if (group is not ItemDropItemGroup boxGroup) + { + return; + } + + item.DropItems.Remove(boxGroup); + await this._persistenceContext!.DeleteAsync(boxGroup).ConfigureAwait(true); + } + + private async Task DeleteGroupAsync(DropItemGroup group) + { + foreach (var map in this._maps) + { + map.DropItemGroups.Remove(group); + } + + foreach (var monster in this._monsters) + { + monster.DropItemGroups.Remove(group); + } + + this._gameConfiguration!.DropItemGroups.Remove(group); + await this._persistenceContext!.DeleteAsync(group).ConfigureAwait(true); + } + + private async Task OnSaveButtonClickAsync() + { + try + { + if (this._persistenceContext is { } context) + { + var success = await context.SaveChangesAsync().ConfigureAwait(true); + var text = success ? Resources.SavedChanges : Resources.NoChangesToSave; + this.ToastService.ShowSuccess(text); + } + else + { + this.ToastService.ShowError(Resources.FailedByUninitializedContext); + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "An unexpected error occurred on save: {Message}", ex.Message); + this.ToastService.ShowError(string.Format(Resources.UnexpectedErrorOccurred, ex.Message)); + } + } + + private async Task OnCancelButtonClickAsync() + { + if (this._persistenceContext?.HasChanges is true) + { + await this.DataSource.DiscardChangesAsync().ConfigureAwait(true); + this._gameConfiguration = null; + await this.LoadDataAsync(this._disposeCts?.Token ?? default).ConfigureAwait(true); + } + } + + /// + /// The drop overview of a monster on a map. It considers the drop item groups in the same way + /// as the , except the quest and character specific groups. + /// + private sealed class MonsterDropOverview + { + /// + /// Initializes a new instance of the class. + /// + /// The monster. + /// The map on which the monster is spawned. + public MonsterDropOverview(MonsterDefinition monster, GameMapDefinition map) + { + this.Monster = monster; + // Not every monster has a level attribute, e.g. destructibles. + this.Level = (int)(monster.Attributes.FirstOrDefault(a => a.AttributeDefinition == Stats.Level)?.Value ?? 0); + var groups = monster.DropItemGroups.ToList(); + if (monster.ObjectKind != NpcObjectKind.Destructible) + { + groups.AddRange(map.DropItemGroups.Where(this.IsRelevant).Except(groups)); + } + + this.Groups = groups.OrderByDescending(g => g.Chance).ToList(); + this.GuaranteedDrops = groups.Count(g => g.Chance >= 1.0); + this.TotalChance = groups.Where(g => g.Chance < 1.0).Sum(g => g.Chance); + } + + /// + /// Gets the monster. + /// + public MonsterDefinition Monster { get; } + + /// + /// Gets the level of the monster. + /// + public int Level { get; } + + /// + /// Gets the drop item groups which apply to the monster. + /// + public IReadOnlyList Groups { get; } + + /// + /// Gets the number of groups which drop always. + /// + public int GuaranteedDrops { get; } + + /// + /// Gets the sum of the chances of the chance based groups, which applies per drop roll. + /// + public double TotalChance { get; } + + /// + /// Gets the chance that nothing drops in a drop roll. + /// + public double NoDropChance => Math.Max(0, 1.0 - this.TotalChance); + + /// + /// Gets the tooltip text which lists the groups with their chance. + /// + public string GroupsTooltip => string.Join(Environment.NewLine, this.Groups.Select(g => $"{g.Chance:P2} {g.Description}")); + + private bool IsRelevant(DropItemGroup group) + { + return (group.MinimumMonsterLevel is not { } minimumLevel || this.Level >= minimumLevel) + && (group.MaximumMonsterLevel is not { } maximumLevel || this.Level <= maximumLevel) + && (group.Monster is null || group.Monster.Equals(this.Monster)); + } + } + + /// + /// Holds the selected objects for which new drop item groups should be added. + /// + private sealed class AdditionSelection + { + /// + /// Gets or sets the monster. + /// + public MonsterDefinition? Monster { get; set; } + + /// + /// Gets or sets the item. + /// + public ItemDefinition? Item { get; set; } + } +} diff --git a/src/Web/AdminPanel/Pages/EditItemDrops.razor.css b/src/Web/AdminPanel/Pages/EditItemDrops.razor.css new file mode 100644 index 000000000..52171a8cb --- /dev/null +++ b/src/Web/AdminPanel/Pages/EditItemDrops.razor.css @@ -0,0 +1,11 @@ +.item-drops-toolbar { + position: sticky; + top: 0; + z-index: 10; + padding: 0.5rem 0; + background-color: var(--bs-body-bg, #fff); +} + +.monster-max-drops { + width: 6rem; +} diff --git a/src/Web/AdminPanel/Properties/Resources.Designer.cs b/src/Web/AdminPanel/Properties/Resources.Designer.cs index 0fa03e26e..0d876b6ea 100644 --- a/src/Web/AdminPanel/Properties/Resources.Designer.cs +++ b/src/Web/AdminPanel/Properties/Resources.Designer.cs @@ -150,6 +150,42 @@ public static string ActiveOfflinePlayer { } } + /// + /// Looks up a localized string similar to Add chance (%). + /// + public static string AddChancePercent { + get { + return ResourceManager.GetString("AddChancePercent", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add drop item group. + /// + public static string AddDropItemGroup { + get { + return ResourceManager.GetString("AddDropItemGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add drops for another item (e.g. a box). + /// + public static string AddDropsForItem { + get { + return ResourceManager.GetString("AddDropsForItem", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Add drops for another monster. + /// + public static string AddDropsForMonster { + get { + return ResourceManager.GetString("AddDropsForMonster", resourceCulture); + } + } + /// /// Looks up a localized string similar to Add New. /// @@ -159,6 +195,15 @@ public static string AddNew { } } + /// + /// Looks up a localized string similar to Adds randomly. + /// + public static string AddsRandomly { + get { + return ResourceManager.GetString("AddsRandomly", resourceCulture); + } + } + /// /// Looks up a localized string similar to Admin Users. /// @@ -330,6 +375,33 @@ public static string ArchivedSessions { } } + /// + /// Looks up a localized string similar to Are you sure?. + /// + public static string AreYouSure { + get { + return ResourceManager.GetString("AreYouSure", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assigned maps. + /// + public static string AssignedMaps { + get { + return ResourceManager.GetString("AssignedMaps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Assign to all maps. + /// + public static string AssignToAllMaps { + get { + return ResourceManager.GetString("AssignToAllMaps", resourceCulture); + } + } + /// /// Looks up a localized string similar to Authenticator code. /// @@ -420,6 +492,24 @@ public static string CapturedPackets { } } + /// + /// Looks up a localized string similar to The chance that this group is selected for a drop, from 0 to 100 %. A chance of 100 % means that the group drops always, in addition to the chance based groups.. + /// + public static string ChanceHint { + get { + return ResourceManager.GetString("ChanceHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Chance (%). + /// + public static string ChancePercent { + get { + return ResourceManager.GetString("ChancePercent", resourceCulture); + } + } + /// /// Looks up a localized string similar to Change password. /// @@ -771,6 +861,15 @@ public static string DeleteArchivedSessionQuestion { } } + /// + /// Looks up a localized string similar to You're about to delete the drop item group '{0}'. Are you sure?. + /// + public static string DeleteDropItemGroupQuestion { + get { + return ResourceManager.GetString("DeleteDropItemGroupQuestion", resourceCulture); + } + } + /// /// Looks up a localized string similar to Couldn't delete '{0}', probably because it's referenced by another object. For details, see log. /// @@ -780,6 +879,15 @@ public static string DeleteFailedReferenced { } } + /// + /// Looks up a localized string similar to Description. + /// + public static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to <strong>The Development environment shouldn't be enabled for deployed applications.</strong> /// It can result in displaying sensitive information from exceptions to end users. @@ -882,6 +990,15 @@ public static string DownloadFile { } } + /// + /// Looks up a localized string similar to Drop effect. + /// + public static string DropEffect { + get { + return ResourceManager.GetString("DropEffect", resourceCulture); + } + } + /// /// Looks up a localized string similar to Drop item groups. /// @@ -891,6 +1008,42 @@ public static string DropItemGroups { } } + /// + /// Looks up a localized string similar to The drop item group '{0}' can't be deleted, because it's still used by: {1}. + /// + public static string DropItemGroupStillUsed { + get { + return ResourceManager.GetString("DropItemGroupStillUsed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop level. + /// + public static string DropLevel { + get { + return ResourceManager.GetString("DropLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The minimum monster level at which the item can drop randomly.. + /// + public static string DropLevelHint { + get { + return ResourceManager.GetString("DropLevelHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drops from monsters. + /// + public static string DropsFromMonsters { + get { + return ResourceManager.GetString("DropsFromMonsters", resourceCulture); + } + } + /// /// Looks up a localized string similar to Duplicate. /// @@ -918,6 +1071,15 @@ public static string Edit { } } + /// + /// Looks up a localized string similar to Edit details. + /// + public static string EditDetails { + get { + return ResourceManager.GetString("EditDetails", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enable. /// @@ -954,6 +1116,24 @@ public static string ErrorDuplicating { } } + /// + /// Looks up a localized string similar to Excellent item drop level delta. + /// + public static string ExcellentItemDropLevelDelta { + get { + return ResourceManager.GetString("ExcellentItemDropLevelDelta", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A monster must be at least this many levels above the drop level of an item, so that the item can drop as excellent item.. + /// + public static string ExcellentItemDropLevelDeltaHint { + get { + return ResourceManager.GetString("ExcellentItemDropLevelDeltaHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Export JSON Backup. /// @@ -1098,6 +1278,15 @@ public static string GameConfiguration { } } + /// + /// Looks up a localized string similar to Map. + /// + public static string GameMap { + get { + return ResourceManager.GetString("GameMap", resourceCulture); + } + } + /// /// Looks up a localized string similar to Game maps. /// @@ -1143,6 +1332,15 @@ public static string General { } } + /// + /// Looks up a localized string similar to General settings. + /// + public static string GeneralDropSettings { + get { + return ResourceManager.GetString("GeneralDropSettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Generate new recovery codes. /// @@ -1188,6 +1386,15 @@ public static string GlobalMessageSent { } } + /// + /// Looks up a localized string similar to Guaranteed drops. + /// + public static string GuaranteedDrops { + get { + return ResourceManager.GetString("GuaranteedDrops", resourceCulture); + } + } + /// /// Looks up a localized string similar to Hide entry form. /// @@ -1296,6 +1503,105 @@ public static string InvalidTwoFactorCode { } } + /// + /// Looks up a localized string similar to Item. + /// + public static string Item { + get { + return ResourceManager.GetString("Item", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item box drops. + /// + public static string ItemBoxDrops { + get { + return ResourceManager.GetString("ItemBoxDrops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Defines what drops when a player drops an item like a box (e.g. Box of Luck, Box of Kundun). One of the groups of the matching source item level is selected by its chance.. + /// + public static string ItemBoxDropsDescription { + get { + return ResourceManager.GetString("ItemBoxDropsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item drop duration (seconds). + /// + public static string ItemDropDurationSeconds { + get { + return ResourceManager.GetString("ItemDropDurationSeconds", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item Drops. + /// + public static string ItemDrops { + get { + return ResourceManager.GetString("ItemDrops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Configure all item drops on one page. Changes are only applied after saving.. + /// + public static string ItemDropsPageDescription { + get { + return ResourceManager.GetString("ItemDropsPageDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item level. + /// + public static string ItemLevel { + get { + return ResourceManager.GetString("ItemLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The item level which is assigned to the dropped item. If empty, the level is determined by the monster level for random items.. + /// + public static string ItemLevelHint { + get { + return ResourceManager.GetString("ItemLevelHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item option. + /// + public static string ItemOption { + get { + return ResourceManager.GetString("ItemOption", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Item option chances. + /// + public static string ItemOptionDropChances { + get { + return ResourceManager.GetString("ItemOptionDropChances", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The chances that options are added randomly to dropped items.. + /// + public static string ItemOptionDropChancesDescription { + get { + return ResourceManager.GetString("ItemOptionDropChancesDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Items. /// @@ -1305,6 +1611,15 @@ public static string Items { } } + /// + /// Looks up a localized string similar to Type. + /// + public static string ItemType { + get { + return ResourceManager.GetString("ItemType", resourceCulture); + } + } + /// /// Looks up a localized string similar to Jewel mixes. /// @@ -1458,6 +1773,24 @@ public static string ManualKeyHint { } } + /// + /// Looks up a localized string similar to Map drop groups. + /// + public static string MapDropItemGroups { + get { + return ResourceManager.GetString("MapDropItemGroups", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop groups which apply to monsters on the assigned maps. Groups which are not assigned to any map don't drop.. + /// + public static string MapDropItemGroupsDescription { + get { + return ResourceManager.GetString("MapDropItemGroupsDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Map Editor. /// @@ -1467,6 +1800,69 @@ public static string MapEditor { } } + /// + /// Looks up a localized string similar to Maximum drop level. + /// + public static string MaximumDropLevel { + get { + return ResourceManager.GetString("MaximumDropLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum monster level at which the item can drop randomly. If empty, there is no upper limit.. + /// + public static string MaximumDropLevelHint { + get { + return ResourceManager.GetString("MaximumDropLevelHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Max. item level. + /// + public static string MaximumItemLevel { + get { + return ResourceManager.GetString("MaximumItemLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum item option level. + /// + public static string MaximumItemOptionLevelDrop { + get { + return ResourceManager.GetString("MaximumItemOptionLevelDrop", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum level of the item option (e.g. +4, +8, +12, +16) which can be added to a dropped item.. + /// + public static string MaximumItemOptionLevelDropHint { + get { + return ResourceManager.GetString("MaximumItemOptionLevelDropHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Max. monster level. + /// + public static string MaximumMonsterLevel { + get { + return ResourceManager.GetString("MaximumMonsterLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Maximum options per item. + /// + public static string MaximumOptionsPerItem { + get { + return ResourceManager.GetString("MaximumOptionsPerItem", resourceCulture); + } + } + /// /// Looks up a localized string similar to Merchants. /// @@ -1512,6 +1908,15 @@ public static string MiniGames { } } + /// + /// Looks up a localized string similar to Mini game '{0}'. + /// + public static string MiniGameUsage { + get { + return ResourceManager.GetString("MiniGameUsage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Required status. /// @@ -1521,6 +1926,24 @@ public static string MinimumCharacterStatus { } } + /// + /// Looks up a localized string similar to Min. item level. + /// + public static string MinimumItemLevel { + get { + return ResourceManager.GetString("MinimumItemLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Min. monster level. + /// + public static string MinimumMonsterLevel { + get { + return ResourceManager.GetString("MinimumMonsterLevel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Minor. /// @@ -1530,6 +1953,78 @@ public static string MinorVersion { } } + /// + /// Looks up a localized string similar to Money. + /// + public static string Money { + get { + return ResourceManager.GetString("Money", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Money amount. + /// + public static string MoneyAmount { + get { + return ResourceManager.GetString("MoneyAmount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Monster. + /// + public static string Monster { + get { + return ResourceManager.GetString("Monster", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop chance overview per monster. + /// + public static string MonsterDropChanceOverview { + get { + return ResourceManager.GetString("MonsterDropChanceOverview", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The drop chances of each monster which is spawned on a map, considering the monster level restrictions of the map drop groups and the monster specific drop groups. The chances apply to each drop roll, a monster rolls up to its maximum item drops. Quest item drops are not included. If the total chance exceeds 100 %, the chances are scaled down proportionally and there is no chance for no drop.. + /// + public static string MonsterDropChanceOverviewDescription { + get { + return ResourceManager.GetString("MonsterDropChanceOverviewDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Monster drops. + /// + public static string MonsterDrops { + get { + return ResourceManager.GetString("MonsterDrops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop groups of specific monsters. They are considered in addition to the map drop groups when the monster is killed.. + /// + public static string MonsterDropsDescription { + get { + return ResourceManager.GetString("MonsterDropsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Level. + /// + public static string MonsterLevel { + get { + return ResourceManager.GetString("MonsterLevel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Monsters. /// @@ -1566,6 +2061,24 @@ public static string NewConfigurationUpdatesAvailable { } } + /// + /// Looks up a localized string similar to New drop item group. + /// + public static string NewDropItemGroup { + get { + return ResourceManager.GetString("NewDropItemGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop of {0}. + /// + public static string NewDropItemGroupOf { + get { + return ResourceManager.GetString("NewDropItemGroupOf", resourceCulture); + } + } + /// /// Looks up a localized string similar to No. /// @@ -1638,6 +2151,24 @@ public static string NoConnectionsFound { } } + /// + /// Looks up a localized string similar to No drop. + /// + public static string NoDropChance { + get { + return ResourceManager.GetString("NoDropChance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There are no drop item groups.. + /// + public static string NoDropItemGroups { + get { + return ResourceManager.GetString("NoDropItemGroups", resourceCulture); + } + } + /// /// Looks up a localized string similar to No initialized data found!. /// @@ -1647,6 +2178,15 @@ public static string NoInitializedDataFound { } } + /// + /// Looks up a localized string similar to No items. + /// + public static string NoItems { + get { + return ResourceManager.GetString("NoItems", resourceCulture); + } + } + /// /// Looks up a localized string similar to No log entries found.. /// @@ -1692,6 +2232,24 @@ public static string NotCreated { } } + /// + /// Looks up a localized string similar to Maximum item drops. + /// + public static string NumberOfMaximumItemDrops { + get { + return ResourceManager.GetString("NumberOfMaximumItemDrops", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The maximum number of items which this monster drops when it's killed.. + /// + public static string NumberOfMaximumItemDropsHint { + get { + return ResourceManager.GetString("NumberOfMaximumItemDropsHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Observe. /// @@ -1737,6 +2295,15 @@ public static string OnlineCount { } } + /// + /// Looks up a localized string similar to Only for monster. + /// + public static string OnlyForMonster { + get { + return ResourceManager.GetString("OnlyForMonster", resourceCulture); + } + } + /// /// Looks up a localized string similar to OpenMU AdminPanel. /// @@ -1953,6 +2520,87 @@ public static string PluginType { } } + /// + /// Looks up a localized string similar to Possible items. + /// + public static string PossibleItems { + get { + return ResourceManager.GetString("PossibleItems", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If no items are selected, random items are dropped depending on the type and monster level.. + /// + public static string PossibleItemsHint { + get { + return ResourceManager.GetString("PossibleItemsHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quest and event drop groups. + /// + public static string QuestAndEventDropItemGroups { + get { + return ResourceManager.GetString("QuestAndEventDropItemGroups", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Drop groups which are used by quests or mini game rewards.. + /// + public static string QuestAndEventDropItemGroupsDescription { + get { + return ResourceManager.GetString("QuestAndEventDropItemGroupsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Quest '{0}'. + /// + public static string QuestUsage { + get { + return ResourceManager.GetString("QuestUsage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Random ancient items. + /// + public static string RandomAncientItems { + get { + return ResourceManager.GetString("RandomAncientItems", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Random items by monster level. + /// + public static string RandomItemsByMonsterLevel { + get { + return ResourceManager.GetString("RandomItemsByMonsterLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Randomly dropped items. + /// + public static string RandomlyDroppedItems { + get { + return ResourceManager.GetString("RandomlyDroppedItems", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Defines which items can drop as random items (e.g. by the common and excellent drop groups) and at which monster levels.. + /// + public static string RandomlyDroppedItemsDescription { + get { + return ResourceManager.GetString("RandomlyDroppedItemsDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Raw Data. /// @@ -2070,6 +2718,24 @@ public static string Remove { } } + /// + /// Looks up a localized string similar to Remove from all maps. + /// + public static string RemoveFromAllMaps { + get { + return ResourceManager.GetString("RemoveFromAllMaps", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove from monster. + /// + public static string RemoveFromMonster { + get { + return ResourceManager.GetString("RemoveFromMonster", resourceCulture); + } + } + /// /// Looks up a localized string similar to Remove Server. /// @@ -2088,6 +2754,15 @@ public static string Required { } } + /// + /// Looks up a localized string similar to Required character level. + /// + public static string RequiredCharacterLevel { + get { + return ResourceManager.GetString("RequiredCharacterLevel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Reset second factor. /// @@ -2340,6 +3015,15 @@ public static string SetUpAuthenticator { } } + /// + /// Looks up a localized string similar to Drop money (instead of adding it to the inventory directly). + /// + public static string ShouldDropMoney { + get { + return ResourceManager.GetString("ShouldDropMoney", resourceCulture); + } + } + /// /// Looks up a localized string similar to Show entry form. /// @@ -2358,6 +3042,15 @@ public static string ShowingXOfYLines { } } + /// + /// Looks up a localized string similar to Show only items which drop from monsters. + /// + public static string ShowOnlyDroppableItems { + get { + return ResourceManager.GetString("ShowOnlyDroppableItems", resourceCulture); + } + } + /// /// Looks up a localized string similar to Signed in as. /// @@ -2403,6 +3096,24 @@ public static string SocketNumber { } } + /// + /// Looks up a localized string similar to Source item level. + /// + public static string SourceItemLevel { + get { + return ResourceManager.GetString("SourceItemLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The level of the dropped item (e.g. the box) for which this group applies.. + /// + public static string SourceItemLevelHint { + get { + return ResourceManager.GetString("SourceItemLevelHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Start. /// @@ -2511,6 +3222,24 @@ public static string ToServer { } } + /// + /// Looks up a localized string similar to Total chance. + /// + public static string TotalChance { + get { + return ResourceManager.GetString("TotalChance", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The total chance exceeds 100 %. The chances are scaled down proportionally.. + /// + public static string TotalChanceExceedsHint { + get { + return ResourceManager.GetString("TotalChanceExceedsHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Total Players. /// @@ -2718,6 +3447,15 @@ public static string UseAuthenticatorCode { } } + /// + /// Looks up a localized string similar to Used by. + /// + public static string UsedBy { + get { + return ResourceManager.GetString("UsedBy", resourceCulture); + } + } + /// /// Looks up a localized string similar to The user has been created.. /// diff --git a/src/Web/AdminPanel/Properties/Resources.resx b/src/Web/AdminPanel/Properties/Resources.resx index 000473472..68ad712df 100644 --- a/src/Web/AdminPanel/Properties/Resources.resx +++ b/src/Web/AdminPanel/Properties/Resources.resx @@ -1254,4 +1254,250 @@ Party + + Add chance (%) + + + Add drop item group + + + Add drops for another item (e.g. a box) + + + Add drops for another monster + + + Adds randomly + + + Are you sure? + + + Assign to all maps + + + Assigned maps + + + The chance that this group is selected for a drop, from 0 to 100 %. A chance of 100 % means that the group drops always, in addition to the chance based groups. + + + Chance (%) + + + You're about to delete the drop item group '{0}'. Are you sure? + + + Description + + + Drop effect + + + The drop item group '{0}' can't be deleted, because it's still used by: {1} + + + Drop level + + + The minimum monster level at which the item can drop randomly. + + + Drops from monsters + + + Edit details + + + Excellent item drop level delta + + + A monster must be at least this many levels above the drop level of an item, so that the item can drop as excellent item. + + + Map + + + General settings + + + Guaranteed drops + + + Item + + + Item box drops + + + Defines what drops when a player drops an item like a box (e.g. Box of Luck, Box of Kundun). One of the groups of the matching source item level is selected by its chance. + + + Item drop duration (seconds) + + + Item Drops + + + Configure all item drops on one page. Changes are only applied after saving. + + + Item level + + + The item level which is assigned to the dropped item. If empty, the level is determined by the monster level for random items. + + + Item option + + + Item option chances + + + The chances that options are added randomly to dropped items. + + + Type + + + Map drop groups + + + Drop groups which apply to monsters on the assigned maps. Groups which are not assigned to any map don't drop. + + + Maximum drop level + + + The maximum monster level at which the item can drop randomly. If empty, there is no upper limit. + + + Max. item level + + + Maximum item option level + + + The maximum level of the item option (e.g. +4, +8, +12, +16) which can be added to a dropped item. + + + Max. monster level + + + Maximum options per item + + + Mini game '{0}' + + + Min. item level + + + Min. monster level + + + Money + + + Money amount + + + Monster drops + + + Drop groups of specific monsters. They are considered in addition to the map drop groups when the monster is killed. + + + New drop item group + + + Drop of {0} + + + No drop + + + There are no drop item groups. + + + No items + + + Maximum item drops + + + The maximum number of items which this monster drops when it's killed. + + + Only for monster + + + Possible items + + + If no items are selected, random items are dropped depending on the type and monster level. + + + Quest and event drop groups + + + Drop groups which are used by quests or mini game rewards. + + + Quest '{0}' + + + Random ancient items + + + Random items by monster level + + + Randomly dropped items + + + Defines which items can drop as random items (e.g. by the common and excellent drop groups) and at which monster levels. + + + Remove from all maps + + + Remove from monster + + + Required character level + + + Drop money (instead of adding it to the inventory directly) + + + Show only items which drop from monsters + + + Source item level + + + The level of the dropped item (e.g. the box) for which this group applies. + + + Total chance + + + The total chance exceeds 100 %. The chances are scaled down proportionally. + + + Used by + + + Monster + + + Level + + + Drop chance overview per monster + + + The drop chances of each monster which is spawned on a map, considering the monster level restrictions of the map drop groups and the monster specific drop groups. The chances apply to each drop roll, a monster rolls up to its maximum item drops. Quest item drops are not included. If the total chance exceeds 100 %, the chances are scaled down proportionally and there is no chance for no drop. +