mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
配置页改为冷却双栏与职业级物品列表
将配置页「法术」改为「冷却」(左技能冷却、右专精物品冷却),「物品」改为职业级 Fuyutsui.itemsList 编辑;保存时缺表则在 spellsList 后插入。 Co-authored-by: Wayne-Arasaka <waynebian01@users.noreply.github.com>
This commit is contained in:
@@ -84,9 +84,12 @@ wow_process.txt 目标游戏进程名列表;构建时复制,运行期间每
|
||||
[Infrastructure/ClassBlocksStore.cs](Infrastructure/ClassBlocksStore.cs) 读写 `class/*.lua` 中的 `Fuyutsui.ClassBlocks` 表。每个职业一个 Lua 文件,按专精 ID 分块,包含:
|
||||
- **States**(状态字段):分平面列表或按 `"状态"/"目标"/"焦点"` 分类(现代格式)
|
||||
- **Auras**(光环):5 桶——玩家/目标有害/目标有益/焦点有害/焦点有益
|
||||
- **Spells**(法术):ID、名称、充能、施法计数、强制已知、法术书
|
||||
- **Spells**(技能冷却):ID、名称、充能、施法计数、强制已知、法术书
|
||||
- **Items**(物品冷却):专精级 `[itemId] = { name, isEquipped }`
|
||||
- **Group**(队伍):人数/生命百分比/角色/驱散 + 队伍光环列表
|
||||
|
||||
同文件另有职业级 `Fuyutsui.spellsList` 与 `Fuyutsui.itemsList`(`[itemId] = { name }`),与专精冷却表分开。
|
||||
|
||||
字段名从 [Infrastructure/ClassStateCatalog.cs](Infrastructure/ClassStateCatalog.cs) 的静态目录验证,不允许自由输入。
|
||||
|
||||
### 宏存储(ClassMacros)
|
||||
@@ -99,7 +102,7 @@ wow_process.txt 目标游戏进程名列表;构建时复制,运行期间每
|
||||
|
||||
### UI 编辑器
|
||||
|
||||
- [UI/ClassConfigEditorControl.cs](UI/ClassConfigEditorControl.cs):左侧职业列表 + 右侧按专精切换的六页编辑器(状态/光环/法术/物品/队伍/技能列表),状态字段用 `ClassStateCatalog` 驱动的 `ComboBoxColumn`。
|
||||
- [UI/ClassConfigEditorControl.cs](UI/ClassConfigEditorControl.cs):左侧职业列表 + 右侧按专精切换的六页编辑器(状态/光环/冷却/物品列表/队伍/技能列表),状态字段用 `ClassStateCatalog` 驱动的 `ComboBoxColumn`。
|
||||
- [UI/ClassMacrosEditorControl.cs](UI/ClassMacrosEditorControl.cs):左侧职业列表 + 右侧三页编辑器(动态宏/静态宏/特殊宏),偏移提示显示槽位编号计算。
|
||||
- 两个编辑器均接受 `Func<string?>` 项目路径解析器 + `Func<string, Task<string?>>` 保存回调,由 `MainForm` 在构造时注入。保存流程:编辑器调 `Store.Save()` → 传入已保存文件路径 → 重新生成 config/keymap → 单文件部署游戏 → 重启运行时;部署失败返回说明,但不回滚本地文件。
|
||||
- 配置更新的多个入口通过任务尾队列串行执行;运行时重启会等待该队列稳定,主窗口关闭也会等待正在写盘的转换和部署完成。新增同步入口必须继续走这条队列。
|
||||
|
||||
@@ -7,12 +7,13 @@ namespace Shigure;
|
||||
|
||||
/// <summary>
|
||||
/// 读写 Fuyutsui class/*.lua 中的 ClassBlocks(states/auras/spells/items/group),
|
||||
/// 同时读写 spellsList;保存时替换 ClassBlocks 表字面量,并原位更新 spellsList 中已编辑的条目。
|
||||
/// 同时读写 spellsList 与 itemsList;保存时替换 ClassBlocks 表字面量,并原位更新列表条目。
|
||||
/// </summary>
|
||||
internal static class ClassBlocksStore
|
||||
{
|
||||
public const string AssignmentName = "Fuyutsui.ClassBlocks";
|
||||
public const string SpellsListAssignmentName = "Fuyutsui.spellsList";
|
||||
public const string ItemsListAssignmentName = "Fuyutsui.itemsList";
|
||||
private static readonly string[] StateCategories =
|
||||
[
|
||||
ClassStateCatalog.CategoryState,
|
||||
@@ -38,6 +39,8 @@ internal static class ClassBlocksStore
|
||||
public Dictionary<int, SpecBlocks> Specs { get; set; } = new();
|
||||
public List<SpellsListEntry> SpellsList { get; set; } = new();
|
||||
public HashSet<long> DeletedSpellsListOriginalIds { get; } = new();
|
||||
public List<ItemsListEntry> ItemsList { get; set; } = new();
|
||||
public HashSet<long> DeletedItemsListOriginalIds { get; } = new();
|
||||
public bool IsModernFormat { get; set; }
|
||||
}
|
||||
|
||||
@@ -51,6 +54,14 @@ internal static class ClassBlocksStore
|
||||
public string OriginalName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ItemsListEntry
|
||||
{
|
||||
public long ItemId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long OriginalItemId { get; set; }
|
||||
public string OriginalName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SpecBlocks
|
||||
{
|
||||
public bool NestedStates { get; set; } = true;
|
||||
@@ -140,6 +151,7 @@ internal static class ClassBlocksStore
|
||||
}
|
||||
|
||||
var spellsList = ParseSpellsList(ExtractAssignedTable(source, SpellsListAssignmentName));
|
||||
var itemsList = ParseItemsList(ExtractAssignedTable(source, ItemsListAssignmentName));
|
||||
return new ClassFileDocument
|
||||
{
|
||||
FilePath = filePath,
|
||||
@@ -148,6 +160,7 @@ internal static class ClassBlocksStore
|
||||
TableEndExclusive = end,
|
||||
Specs = specs,
|
||||
SpellsList = spellsList,
|
||||
ItemsList = itemsList,
|
||||
IsModernFormat = modern
|
||||
};
|
||||
}
|
||||
@@ -192,6 +205,39 @@ internal static class ClassBlocksStore
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<ItemsListEntry> ParseItemsList(TableValue? table)
|
||||
{
|
||||
var result = new List<ItemsListEntry>();
|
||||
if (table is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in table.Entries)
|
||||
{
|
||||
if (key is not long itemId || itemId <= 0 || value is not TableValue item)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = item.GetString("name")?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new ItemsListEntry
|
||||
{
|
||||
ItemId = itemId,
|
||||
Name = name,
|
||||
OriginalItemId = itemId,
|
||||
OriginalName = name
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void Save(ClassFileDocument document)
|
||||
{
|
||||
if (!document.IsModernFormat)
|
||||
@@ -203,6 +249,10 @@ internal static class ClassBlocksStore
|
||||
document.SourceText,
|
||||
document.SpellsList,
|
||||
document.DeletedSpellsListOriginalIds);
|
||||
updated = UpdateItemsListEntries(
|
||||
updated,
|
||||
document.ItemsList,
|
||||
document.DeletedItemsListOriginalIds);
|
||||
if (!TryExtractAssignedTable(updated, AssignmentName, out _, out var classBlocksStart, out var classBlocksEnd))
|
||||
{
|
||||
throw new InvalidOperationException("保存前无法重新定位 ClassBlocks 表。");
|
||||
@@ -230,6 +280,13 @@ internal static class ClassBlocksStore
|
||||
}
|
||||
|
||||
document.DeletedSpellsListOriginalIds.Clear();
|
||||
foreach (var item in document.ItemsList)
|
||||
{
|
||||
item.OriginalItemId = item.ItemId;
|
||||
item.OriginalName = item.Name;
|
||||
}
|
||||
|
||||
document.DeletedItemsListOriginalIds.Clear();
|
||||
}
|
||||
|
||||
public sealed class ItemEntry
|
||||
@@ -346,6 +403,143 @@ internal static class ClassBlocksStore
|
||||
return source[..tableStart] + updatedTable + source[tableEnd..];
|
||||
}
|
||||
|
||||
private static string UpdateItemsListEntries(
|
||||
string source,
|
||||
IReadOnlyList<ItemsListEntry> entries,
|
||||
IReadOnlySet<long> deletedOriginalIds)
|
||||
{
|
||||
var newline = source.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
|
||||
var newEntries = entries.Where(entry => entry.OriginalItemId == 0).ToArray();
|
||||
var changedEntries = entries
|
||||
.Where(entry => entry.OriginalItemId != 0
|
||||
&& (entry.ItemId != entry.OriginalItemId
|
||||
|| !string.Equals(entry.Name, entry.OriginalName, StringComparison.Ordinal)))
|
||||
.ToDictionary(entry => entry.OriginalItemId);
|
||||
if (!TryExtractAssignedTable(source, ItemsListAssignmentName, out _, out var tableStart, out var tableEnd))
|
||||
{
|
||||
if (!TryExtractAssignedTable(source, SpellsListAssignmentName, out _, out _, out var spellsEnd))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"当前文件中未找到 {SpellsListAssignmentName},无法写入物品列表。");
|
||||
}
|
||||
|
||||
return source[..spellsEnd]
|
||||
+ newline
|
||||
+ newline
|
||||
+ SerializeItemsListTable(entries, newline)
|
||||
+ source[spellsEnd..];
|
||||
}
|
||||
|
||||
if (changedEntries.Count == 0 && newEntries.Length == 0 && deletedOriginalIds.Count == 0)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var tableText = source[tableStart..tableEnd];
|
||||
var updatedOriginalIds = new HashSet<long>();
|
||||
var deletedIdsFound = new HashSet<long>();
|
||||
var pattern = new Regex(
|
||||
"""^(?<prefix>[ \t]*\[[ \t]*)(?<itemId>\d+)(?<beforeName>[ \t]*\][ \t]*=[ \t]*\{[ \t]*name[ \t]*=[ \t]*)(?<name>"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')(?<suffix>[^\r\n]*)(?<lineEnd>\r?\n|$)""",
|
||||
RegexOptions.Multiline | RegexOptions.CultureInvariant);
|
||||
var updatedTable = pattern.Replace(tableText, match =>
|
||||
{
|
||||
if (!long.TryParse(match.Groups["itemId"].Value, NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var originalItemId))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
if (deletedOriginalIds.Contains(originalItemId))
|
||||
{
|
||||
deletedIdsFound.Add(originalItemId);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!changedEntries.TryGetValue(originalItemId, out var entry))
|
||||
{
|
||||
updatedOriginalIds.Add(originalItemId);
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
updatedOriginalIds.Add(originalItemId);
|
||||
var quotedName = match.Groups["name"].Value;
|
||||
var quote = quotedName[0];
|
||||
return match.Groups["prefix"].Value
|
||||
+ entry.ItemId.ToString(CultureInfo.InvariantCulture)
|
||||
+ match.Groups["beforeName"].Value
|
||||
+ quote
|
||||
+ EscapeLuaString(entry.Name, quote)
|
||||
+ quote
|
||||
+ match.Groups["suffix"].Value
|
||||
+ match.Groups["lineEnd"].Value;
|
||||
});
|
||||
|
||||
var missing = changedEntries.Keys.Where(id => !updatedOriginalIds.Contains(id)).ToArray();
|
||||
if (missing.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"无法在 {ItemsListAssignmentName} 中定位 itemId {string.Join(", ", missing)} 的原始条目。");
|
||||
}
|
||||
|
||||
var missingDeleted = deletedOriginalIds.Where(id => !deletedIdsFound.Contains(id)).ToArray();
|
||||
if (missingDeleted.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"无法在 {ItemsListAssignmentName} 中定位待删除的 itemId {string.Join(", ", missingDeleted)}。");
|
||||
}
|
||||
|
||||
if (newEntries.Length > 0)
|
||||
{
|
||||
var closingBraceIndex = updatedTable.Length - 1;
|
||||
var closingLineBreak = updatedTable.LastIndexOf('\n', Math.Max(0, closingBraceIndex - 1));
|
||||
var insertionIndex = closingLineBreak >= 0 ? closingLineBreak + 1 : closingBraceIndex;
|
||||
var insertion = new StringBuilder();
|
||||
if (closingLineBreak < 0)
|
||||
{
|
||||
insertion.Append(newline);
|
||||
}
|
||||
|
||||
foreach (var entry in newEntries.OrderBy(entry => entry.ItemId))
|
||||
{
|
||||
insertion.Append(" [")
|
||||
.Append(entry.ItemId.ToString(CultureInfo.InvariantCulture))
|
||||
.Append("] = { name = \"")
|
||||
.Append(EscapeLuaString(entry.Name, '"'))
|
||||
.Append("\" },")
|
||||
.Append(newline);
|
||||
}
|
||||
|
||||
updatedTable = updatedTable.Insert(insertionIndex, insertion.ToString());
|
||||
}
|
||||
|
||||
return source[..tableStart] + updatedTable + source[tableEnd..];
|
||||
}
|
||||
|
||||
private static string SerializeItemsListTable(IReadOnlyList<ItemsListEntry> entries, string newline)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(ItemsListAssignmentName).Append(" = {");
|
||||
if (entries.Count == 0)
|
||||
{
|
||||
builder.Append('}');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
builder.Append(newline);
|
||||
foreach (var entry in entries.OrderBy(item => item.ItemId))
|
||||
{
|
||||
builder.Append(" [")
|
||||
.Append(entry.ItemId.ToString(CultureInfo.InvariantCulture))
|
||||
.Append("] = { name = \"")
|
||||
.Append(EscapeLuaString(entry.Name, '"'))
|
||||
.Append("\" },")
|
||||
.Append(newline);
|
||||
}
|
||||
|
||||
builder.Append('}');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeLuaString(string value, char quote)
|
||||
{
|
||||
var escaped = value
|
||||
|
||||
@@ -46,7 +46,7 @@ Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyut
|
||||
|
||||
- 置顶浮动条:显示程序名、当前职业图标颜色和逻辑状态,提供 `开启/关闭`、`设置`、`✕` 按钮。窗口可拖动和缩放,显示后自动启动运行循环。
|
||||
- `通用`:设置触发键和发送模式;从项目 Fuyutsui 更新配置并同步游戏插件;按实时环境选择模块,或按职业、专精、英雄天赋和队伍类型指定默认模块;从 GitHub 按需下载或更新技能/物品图标数据包。
|
||||
- `配置`:直接编辑项目 `Fuyutsui/class/*.lua` 中的 `ClassBlocks`,包括状态、光环、法术、物品和队伍字段;物品页为当前专精列表与全量物品数据库双栏,维护 `itemId`、名称及是否装备中;技能列表页可编辑 `Fuyutsui.spellsList` 中索引 1–100 的法术 ID、索引和名称,也可从技能数据库添加。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/items/group` 格式。
|
||||
- `配置`:直接编辑项目 `Fuyutsui/class/*.lua` 中的 `ClassBlocks`,包括状态、光环、冷却(技能冷却与当前专精物品冷却)和队伍字段;物品列表页编辑职业级 `Fuyutsui.itemsList`,并可从全量物品数据库添加;技能列表页可编辑 `Fuyutsui.spellsList` 中索引 1–100 的法术 ID、索引和名称,也可从技能数据库添加。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/items/group` 格式。
|
||||
- `宏`:编辑项目 `Fuyutsui/core/classmacros.lua` 中各职业的动态宏、静态宏和特殊宏。动态宏每项占用 30 个团队点名槽位;特殊宏的技能名必须手工填写,不从宏正文解析,生成映射时固定为无目标、无宏条件。
|
||||
- `模块`:新建、编辑、删除本地模块,维护作者、推荐天赋、匹配条件、动态字段和有序规则。规则支持拖拽、上移、下移、复制与插入。
|
||||
- `状态`:分栏显示基础状态、`auras`、`spells` 和模块计算出的动态单位/数值。
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed record ClassConfigPostSaveResult(
|
||||
|
||||
/// <summary>
|
||||
/// 图形化编辑 Fuyutsui class/*.lua 的 ClassBlocks(states / auras / spells / items / group),
|
||||
/// 并编辑同文件中的 spellsList。
|
||||
/// 并编辑同文件中的 spellsList 与 itemsList。
|
||||
/// </summary>
|
||||
public sealed class ClassConfigEditorControl : UserControl
|
||||
{
|
||||
@@ -32,6 +32,8 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private readonly DataGridView _spellsGrid = new();
|
||||
private readonly DataGridView _itemsGrid = new();
|
||||
private readonly TextBox _itemsSearchBox = new();
|
||||
private readonly DataGridView _itemsListGrid = new();
|
||||
private readonly TextBox _itemsListSearchBox = new();
|
||||
private readonly DataGridView _itemDatabaseGrid = new();
|
||||
private readonly TextBox _itemDatabaseFilterBox = new();
|
||||
private readonly Label _itemDatabaseStatusLabel = new();
|
||||
@@ -72,6 +74,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private int? _currentSpecId;
|
||||
private bool _suppressUi;
|
||||
private bool _dirty;
|
||||
private int _editorTabIndex = -1;
|
||||
|
||||
internal event Action<bool>? DirtyStateChanged;
|
||||
internal bool HasUnsavedChanges => _dirty;
|
||||
@@ -382,21 +385,28 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
|
||||
var tabs = new UiPillTab[6];
|
||||
var selectedIndex = -1;
|
||||
void SelectTab(int index)
|
||||
{
|
||||
if (selectedIndex == index)
|
||||
if (_editorTabIndex == index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_suppressUi && selectedIndex == 3)
|
||||
if (!_suppressUi && _editorTabIndex == 2)
|
||||
{
|
||||
_spellsGrid.EndEdit();
|
||||
_itemsGrid.EndEdit();
|
||||
WriteBackSpells();
|
||||
WriteBackItems();
|
||||
}
|
||||
|
||||
selectedIndex = index;
|
||||
if (!_suppressUi && _editorTabIndex == 3)
|
||||
{
|
||||
_itemsListGrid.EndEdit();
|
||||
WriteBackItemsList();
|
||||
}
|
||||
|
||||
_editorTabIndex = index;
|
||||
for (var i = 0; i < tabs.Length; i++)
|
||||
{
|
||||
var selected = i == index;
|
||||
@@ -409,7 +419,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
var titles = new[] { "状态", "光环", "法术", "物品", "队伍", "技能列表" };
|
||||
var titles = new[] { "状态", "光环", "冷却", "物品列表", "队伍", "技能列表" };
|
||||
for (var i = 0; i < titles.Length; i++)
|
||||
{
|
||||
var index = i;
|
||||
@@ -522,14 +532,14 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
Padding = new Padding(8, 0, 0, 0)
|
||||
}, 0, 0);
|
||||
|
||||
UiTheme.StyleTextBox(_itemsSearchBox);
|
||||
_itemsSearchBox.Dock = DockStyle.None;
|
||||
_itemsSearchBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_itemsSearchBox.Margin = new Padding(0);
|
||||
_itemsSearchBox.Height = 30;
|
||||
_itemsSearchBox.PlaceholderText = "itemId 或名称";
|
||||
_itemsSearchBox.TextChanged += (_, _) => ApplyItemsFilter();
|
||||
searchCard.Controls.Add(_itemsSearchBox, 1, 0);
|
||||
UiTheme.StyleTextBox(_itemsListSearchBox);
|
||||
_itemsListSearchBox.Dock = DockStyle.None;
|
||||
_itemsListSearchBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_itemsListSearchBox.Margin = new Padding(0);
|
||||
_itemsListSearchBox.Height = 30;
|
||||
_itemsListSearchBox.PlaceholderText = "itemId 或名称";
|
||||
_itemsListSearchBox.TextChanged += (_, _) => ApplyItemsListFilter();
|
||||
searchCard.Controls.Add(_itemsListSearchBox, 1, 0);
|
||||
leftColumn.Controls.Add(searchCard, 0, 0);
|
||||
|
||||
var currentListCard = new UiCardPanel
|
||||
@@ -555,46 +565,45 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
};
|
||||
currentListHeader.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
currentListHeader.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
var currentListTitle = CreateCardTitle("当前专精物品");
|
||||
var currentListTitle = CreateCardTitle("物品列表");
|
||||
currentListTitle.AutoSize = true;
|
||||
currentListTitle.Dock = DockStyle.None;
|
||||
currentListTitle.Anchor = AnchorStyles.Left;
|
||||
currentListHeader.Controls.Add(currentListTitle, 0, 0);
|
||||
var hint = CreateFieldCaption("名称可改为业务别名;图标始终按 itemId 匹配。");
|
||||
var hint = CreateFieldCaption("来自当前职业 Lua 的 Fuyutsui.itemsList。");
|
||||
hint.TextAlign = ContentAlignment.MiddleRight;
|
||||
currentListHeader.Controls.Add(hint, 1, 0);
|
||||
currentListCard.Controls.Add(currentListHeader, 0, 0);
|
||||
|
||||
ConfigureGrid(_itemsGrid, "class-config-items");
|
||||
_itemsGrid.AllowUserToAddRows = false;
|
||||
_itemsGrid.CellContentClick += HandleDeleteClick;
|
||||
_itemsGrid.CellValueChanged += (_, e) =>
|
||||
ConfigureGrid(_itemsListGrid, "class-config-items-list");
|
||||
_itemsListGrid.AllowUserToAddRows = false;
|
||||
_itemsListGrid.CellContentClick += HandleItemsListDeleteClick;
|
||||
_itemsListGrid.CellValueChanged += (_, e) =>
|
||||
{
|
||||
MarkDirty();
|
||||
if (e.RowIndex >= 0 && e.RowIndex < _itemsGrid.Rows.Count)
|
||||
if (e.RowIndex >= 0 && e.RowIndex < _itemsListGrid.Rows.Count)
|
||||
{
|
||||
UpdateItemGridIcon(_itemsGrid.Rows[e.RowIndex]);
|
||||
UpdateItemGridIcon(_itemsListGrid.Rows[e.RowIndex]);
|
||||
}
|
||||
};
|
||||
_itemsGrid.DataError += (_, e) => e.ThrowException = false;
|
||||
_itemsGrid.Columns.Add(CreateSpellIconColumn());
|
||||
_itemsGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
_itemsListGrid.DataError += (_, e) => e.ThrowException = false;
|
||||
_itemsListGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "ItemId",
|
||||
HeaderText = "itemId",
|
||||
Width = 125,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
_itemsGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
_itemsListGrid.Columns.Add(CreateSpellIconColumn());
|
||||
_itemsListGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Name",
|
||||
HeaderText = "名称",
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
_itemsGrid.Columns.Add(CreateSpellCheckColumn("IsEquipped", "是否装备中", 12, 110));
|
||||
_itemsGrid.Columns.Add(CreateDeleteColumn());
|
||||
currentListCard.Controls.Add(_itemsGrid, 0, 1);
|
||||
_itemsListGrid.Columns.Add(CreateDeleteColumn());
|
||||
currentListCard.Controls.Add(_itemsListGrid, 0, 1);
|
||||
leftColumn.Controls.Add(currentListCard, 0, 1);
|
||||
split.Controls.Add(leftColumn, 0, 0);
|
||||
|
||||
@@ -871,21 +880,63 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
|
||||
private Control BuildSpellsPage()
|
||||
{
|
||||
var panel = new TableLayoutPanel
|
||||
var split = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 1,
|
||||
BackColor = UiTheme.SurfaceRaised,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
split.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||||
split.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
|
||||
split.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
var leftColumn = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 3,
|
||||
BackColor = UiTheme.SurfaceRaised
|
||||
RowCount = 2,
|
||||
BackColor = UiTheme.SurfaceRaised,
|
||||
Margin = new Padding(0, 0, 5, 0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 44));
|
||||
leftColumn.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
leftColumn.RowStyles.Add(new RowStyle(SizeType.Absolute, 44));
|
||||
|
||||
var spellCard = new UiCardPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
spellCard.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
|
||||
spellCard.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
var spellHeader = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 1,
|
||||
BackColor = Color.Transparent,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(12, 0, 12, 0)
|
||||
};
|
||||
spellHeader.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
spellHeader.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
var spellTitle = CreateCardTitle("技能冷却");
|
||||
spellTitle.AutoSize = true;
|
||||
spellTitle.Dock = DockStyle.None;
|
||||
spellTitle.Anchor = AnchorStyles.Left;
|
||||
spellHeader.Controls.Add(spellTitle, 0, 0);
|
||||
var textureOrderHint = CreateFieldCaption(
|
||||
"纹理按行排列;充能法术连续占 2 格:冷却 → 充能冷却。最大充能、施法次数只生成横向计数条。");
|
||||
textureOrderHint.Padding = new Padding(8, 0, 0, 0);
|
||||
panel.Controls.Add(textureOrderHint, 0, 0);
|
||||
"充能法术连续占 2 格:冷却 → 充能冷却。");
|
||||
textureOrderHint.TextAlign = ContentAlignment.MiddleRight;
|
||||
spellHeader.Controls.Add(textureOrderHint, 1, 0);
|
||||
spellCard.Controls.Add(spellHeader, 0, 0);
|
||||
|
||||
ConfigureGrid(_spellsGrid, "class-config-spells");
|
||||
_spellsGrid.Columns.Add(CreateSpellIconColumn());
|
||||
@@ -908,9 +959,122 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
};
|
||||
_spellsGrid.UserAddedRow += (_, _) => MarkDirty();
|
||||
_spellsGrid.DataError += (_, e) => e.ThrowException = false;
|
||||
panel.Controls.Add(_spellsGrid, 0, 1);
|
||||
panel.Controls.Add(BuildMoveButtons(_spellsGrid), 0, 2);
|
||||
return panel;
|
||||
spellCard.Controls.Add(_spellsGrid, 0, 1);
|
||||
leftColumn.Controls.Add(spellCard, 0, 0);
|
||||
leftColumn.Controls.Add(BuildMoveButtons(_spellsGrid), 0, 1);
|
||||
split.Controls.Add(leftColumn, 0, 0);
|
||||
|
||||
var rightColumn = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
BackColor = UiTheme.SurfaceRaised,
|
||||
Margin = new Padding(5, 0, 0, 0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
rightColumn.RowStyles.Add(new RowStyle(SizeType.Absolute, 68));
|
||||
rightColumn.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
var searchCard = new UiCardPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 1,
|
||||
Margin = new Padding(0, 0, 0, 10),
|
||||
Padding = new Padding(12, 8, 12, 8)
|
||||
};
|
||||
searchCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 64));
|
||||
searchCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
searchCard.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
searchCard.Controls.Add(new Label
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
Text = "搜索",
|
||||
ForeColor = UiTheme.Text,
|
||||
BackColor = Color.Transparent,
|
||||
Font = new Font(Font.FontFamily, 9F, FontStyle.Bold, GraphicsUnit.Point),
|
||||
TextAlign = ContentAlignment.MiddleLeft,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(8, 0, 0, 0)
|
||||
}, 0, 0);
|
||||
|
||||
UiTheme.StyleTextBox(_itemsSearchBox);
|
||||
_itemsSearchBox.Dock = DockStyle.None;
|
||||
_itemsSearchBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_itemsSearchBox.Margin = new Padding(0);
|
||||
_itemsSearchBox.Height = 30;
|
||||
_itemsSearchBox.PlaceholderText = "itemId 或名称";
|
||||
_itemsSearchBox.TextChanged += (_, _) => ApplyItemsFilter();
|
||||
searchCard.Controls.Add(_itemsSearchBox, 1, 0);
|
||||
rightColumn.Controls.Add(searchCard, 0, 0);
|
||||
|
||||
var itemCard = new UiCardPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
itemCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
itemCard.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
|
||||
itemCard.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
var itemHeader = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 2,
|
||||
RowCount = 1,
|
||||
BackColor = Color.Transparent,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(12, 0, 12, 0)
|
||||
};
|
||||
itemHeader.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
itemHeader.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
var itemTitle = CreateCardTitle("物品冷却");
|
||||
itemTitle.AutoSize = true;
|
||||
itemTitle.Dock = DockStyle.None;
|
||||
itemTitle.Anchor = AnchorStyles.Left;
|
||||
itemHeader.Controls.Add(itemTitle, 0, 0);
|
||||
var itemHint = CreateFieldCaption("名称可改为业务别名;图标始终按 itemId 匹配。");
|
||||
itemHint.TextAlign = ContentAlignment.MiddleRight;
|
||||
itemHeader.Controls.Add(itemHint, 1, 0);
|
||||
itemCard.Controls.Add(itemHeader, 0, 0);
|
||||
|
||||
ConfigureGrid(_itemsGrid, "class-config-items");
|
||||
_itemsGrid.CellContentClick += HandleDeleteClick;
|
||||
_itemsGrid.CellValueChanged += (_, e) =>
|
||||
{
|
||||
MarkDirty();
|
||||
if (e.RowIndex >= 0 && e.RowIndex < _itemsGrid.Rows.Count)
|
||||
{
|
||||
UpdateItemGridIcon(_itemsGrid.Rows[e.RowIndex]);
|
||||
}
|
||||
};
|
||||
_itemsGrid.UserAddedRow += (_, _) => MarkDirty();
|
||||
_itemsGrid.DataError += (_, e) => e.ThrowException = false;
|
||||
_itemsGrid.Columns.Add(CreateSpellIconColumn());
|
||||
_itemsGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "ItemId",
|
||||
HeaderText = "itemId",
|
||||
Width = 125,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
_itemsGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "Name",
|
||||
HeaderText = "名称",
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
_itemsGrid.Columns.Add(CreateSpellCheckColumn("IsEquipped", "是否装备中", 12, 110));
|
||||
_itemsGrid.Columns.Add(CreateDeleteColumn());
|
||||
itemCard.Controls.Add(_itemsGrid, 0, 1);
|
||||
rightColumn.Controls.Add(itemCard, 0, 1);
|
||||
split.Controls.Add(rightColumn, 1, 0);
|
||||
return split;
|
||||
}
|
||||
|
||||
private Control BuildSpellsListPage()
|
||||
@@ -1647,11 +1811,16 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
// spellsList 是用户实际添加/保存技能时使用的名称,优先级高于专精规则中的同 ID 别名。
|
||||
// spellsList / itemsList 是用户实际添加/保存时使用的名称,优先级高于专精规则中的同 ID 别名。
|
||||
foreach (var spell in document.SpellsList)
|
||||
{
|
||||
SpellIconCatalog.Register(spell.SpellId, spell.Name, overwriteIdName: true);
|
||||
}
|
||||
|
||||
foreach (var item in document.ItemsList)
|
||||
{
|
||||
SpellIconCatalog.RegisterItem(item.ItemId, item.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectClassFromList()
|
||||
@@ -1766,6 +1935,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
try
|
||||
{
|
||||
FillSpellsListGrid();
|
||||
FillItemsListGrid();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1924,6 +2094,11 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
|
||||
foreach (DataGridViewRow row in _itemsGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.Visible = string.IsNullOrEmpty(query) || ItemsRowMatches(row, query);
|
||||
}
|
||||
}
|
||||
@@ -2235,112 +2410,80 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"当前物品数据库缺少 itemId {suggestion.ItemId} 的名称,请更新技能/物品数据包后再添加。",
|
||||
"物品",
|
||||
"物品列表",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
AddItemFromDatabase(new ItemSuggestion(suggestion.ItemId, name));
|
||||
AddItemsListFromDatabase(new ItemSuggestion(suggestion.ItemId, name));
|
||||
}
|
||||
|
||||
private void AddItemFromDatabase(ItemSuggestion suggestion)
|
||||
private void AddItemsListFromDatabase(ItemSuggestion suggestion)
|
||||
{
|
||||
if (_currentSpec is null)
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
MessageBox.Show("请先选择一个专精。", "物品", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
MessageBox.Show("请先选择一个职业文件。", "物品列表", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
_itemsGrid.EndEdit();
|
||||
WriteBackItems();
|
||||
if (_currentSpec.Items.Any(item => item.ItemId == suggestion.ItemId))
|
||||
if (!_currentDocument.IsModernFormat)
|
||||
{
|
||||
MessageBox.Show("旧版稀疏索引格式暂不支持添加物品。", "物品列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_itemsListGrid.EndEdit();
|
||||
if (!TryValidateItemsList(out var validationError))
|
||||
{
|
||||
MessageBox.Show(validationError, "物品列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteBackItemsList();
|
||||
if (_currentDocument.ItemsList.Any(item => item.ItemId == suggestion.ItemId))
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"已有此物品:{suggestion.Name}({suggestion.ItemId})",
|
||||
"物品",
|
||||
"物品列表",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentSpec.Items.Any(item =>
|
||||
if (_currentDocument.ItemsList.Any(item =>
|
||||
string.Equals(item.Name, suggestion.Name, StringComparison.Ordinal)))
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"已有同名物品“{suggestion.Name}”。",
|
||||
"物品",
|
||||
"物品列表",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CurrentSpecReservedItemNames().Contains(suggestion.Name))
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"物品名称“{suggestion.Name}”与状态、能量或配置开关字段重名。",
|
||||
"物品",
|
||||
MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new ClassBlocksStore.ItemEntry
|
||||
var entry = new ClassBlocksStore.ItemsListEntry
|
||||
{
|
||||
ItemId = suggestion.ItemId,
|
||||
Name = suggestion.Name,
|
||||
IsEquipped = false
|
||||
Name = suggestion.Name
|
||||
};
|
||||
_currentSpec.Items.Add(entry);
|
||||
_currentDocument.ItemsList.Add(entry);
|
||||
SpellIconCatalog.RegisterItem(suggestion.ItemId, suggestion.Name);
|
||||
|
||||
var rowIndex = _itemsGrid.Rows.Add(
|
||||
SpellIconCatalog.GetItem(suggestion.ItemId)!,
|
||||
var rowIndex = _itemsListGrid.Rows.Add(
|
||||
suggestion.ItemId.ToString(CultureInfo.InvariantCulture),
|
||||
suggestion.Name,
|
||||
false,
|
||||
"×");
|
||||
var row = _itemsGrid.Rows[rowIndex];
|
||||
SpellIconCatalog.GetItem(suggestion.ItemId)!,
|
||||
suggestion.Name);
|
||||
var row = _itemsListGrid.Rows[rowIndex];
|
||||
row.Tag = entry;
|
||||
_itemsSearchBox.Clear();
|
||||
ApplyItemsFilter();
|
||||
_itemsListSearchBox.Clear();
|
||||
ApplyItemsListFilter();
|
||||
row.Selected = true;
|
||||
_itemsGrid.CurrentCell = row.Cells["ItemId"];
|
||||
_itemsGrid.FirstDisplayedScrollingRowIndex = rowIndex;
|
||||
_itemsListGrid.CurrentCell = row.Cells["ItemId"];
|
||||
_itemsListGrid.FirstDisplayedScrollingRowIndex = rowIndex;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private HashSet<string> CurrentSpecReservedItemNames()
|
||||
{
|
||||
var bareNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (_currentSpec is null)
|
||||
{
|
||||
return bareNames;
|
||||
}
|
||||
|
||||
if (_currentSpec.NestedStates)
|
||||
{
|
||||
foreach (var category in new[]
|
||||
{
|
||||
ClassStateCatalog.CategoryState,
|
||||
ClassStateCatalog.CategoryResource,
|
||||
ClassStateCatalog.CategoryConfig
|
||||
})
|
||||
{
|
||||
foreach (var name in _currentSpec.CategorizedStates.GetValueOrDefault(category) ?? [])
|
||||
{
|
||||
bareNames.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bareNames.UnionWith(_currentSpec.FlatStates);
|
||||
}
|
||||
|
||||
return bareNames;
|
||||
}
|
||||
|
||||
private void ReloadStatesGrid()
|
||||
{
|
||||
CloseStateComboDropDown();
|
||||
@@ -2677,6 +2820,43 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void FillItemsListGrid()
|
||||
{
|
||||
_itemsListGrid.Rows.Clear();
|
||||
_itemsListSearchBox.Clear();
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in _currentDocument.ItemsList.OrderBy(entry => entry.ItemId))
|
||||
{
|
||||
SpellIconCatalog.RegisterItem(item.ItemId, item.Name);
|
||||
var rowIndex = _itemsListGrid.Rows.Add(
|
||||
item.ItemId.ToString(CultureInfo.InvariantCulture),
|
||||
SpellIconCatalog.GetItem(item.ItemId)!,
|
||||
item.Name);
|
||||
_itemsListGrid.Rows[rowIndex].Tag = item;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyItemsListFilter()
|
||||
{
|
||||
var query = _itemsListSearchBox.Text.Trim();
|
||||
_itemsListGrid.ClearSelection();
|
||||
_itemsListGrid.CurrentCell = null;
|
||||
|
||||
foreach (DataGridViewRow row in _itemsListGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
row.Visible = string.IsNullOrEmpty(query) || ItemsRowMatches(row, query);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySpellsListFilter()
|
||||
{
|
||||
var query = _spellsListSearchBox.Text.Trim();
|
||||
@@ -3087,6 +3267,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
|
||||
_itemsGrid.EndEdit();
|
||||
_spellsGrid.EndEdit();
|
||||
NormalizeFixedStateNames(_currentSpec);
|
||||
WriteBackStatesCategory(_lastStateCategory);
|
||||
|
||||
@@ -3122,6 +3303,34 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteBackItemsList()
|
||||
{
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (DataGridViewRow row in _itemsListGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow || row.Tag is not ClassBlocksStore.ItemsListEntry entry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!long.TryParse(
|
||||
row.Cells["ItemId"].Value?.ToString()?.Trim(),
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var itemId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.ItemId = itemId;
|
||||
entry.Name = row.Cells["Name"].Value?.ToString()?.Trim() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSpellFromDatabase(SpellSuggestion suggestion)
|
||||
{
|
||||
if (_currentDocument is null)
|
||||
@@ -3218,15 +3427,19 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
grid.Invalidate();
|
||||
}
|
||||
|
||||
foreach (DataGridViewRow row in _itemsGrid.Rows)
|
||||
foreach (var grid in new[] { _itemsGrid, _itemsListGrid })
|
||||
{
|
||||
if (!row.IsNewRow)
|
||||
foreach (DataGridViewRow row in grid.Rows)
|
||||
{
|
||||
UpdateItemGridIcon(row);
|
||||
if (!row.IsNewRow)
|
||||
{
|
||||
UpdateItemGridIcon(row);
|
||||
}
|
||||
}
|
||||
|
||||
grid.Invalidate();
|
||||
}
|
||||
|
||||
_itemsGrid.Invalidate();
|
||||
_itemDatabaseGrid.Invalidate();
|
||||
|
||||
foreach (var grid in new[] { _aurasGrid, _groupAurasGrid })
|
||||
@@ -3295,6 +3508,55 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryValidateItemsList(out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var itemIds = new HashSet<long>();
|
||||
var itemNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (DataGridViewRow row in _itemsListGrid.Rows)
|
||||
{
|
||||
if (row.IsNewRow)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rowNumber = row.Index + 1;
|
||||
if (!long.TryParse(row.Cells["ItemId"].Value?.ToString()?.Trim(), NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var itemId)
|
||||
|| itemId <= 0)
|
||||
{
|
||||
error = $"物品列表第 {rowNumber} 行 itemId 必须是正整数。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = row.Cells["Name"].Value?.ToString()?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
error = $"物品列表第 {rowNumber} 行名称不能为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!itemIds.Add(itemId))
|
||||
{
|
||||
error = $"物品列表 itemId {itemId} 重复。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!itemNames.Add(name))
|
||||
{
|
||||
error = $"物品列表名称“{name}”重复。";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryValidateItems(out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
@@ -3619,7 +3881,9 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
try
|
||||
{
|
||||
_spellsListGrid.EndEdit();
|
||||
_itemsListGrid.EndEdit();
|
||||
_statesGrid.EndEdit();
|
||||
_spellsGrid.EndEdit();
|
||||
_itemsGrid.EndEdit();
|
||||
if (!TryValidateSpellsList(out var validationError))
|
||||
{
|
||||
@@ -3628,16 +3892,24 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryValidateItemsList(out validationError))
|
||||
{
|
||||
MessageBox.Show(validationError, "物品列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_statusLabel.Text = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换分类前把当前状态表写回。
|
||||
CommitCurrentSpecFromUi();
|
||||
if (!TryValidateItems(out validationError))
|
||||
{
|
||||
MessageBox.Show(validationError, "物品", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
MessageBox.Show(validationError, "物品冷却", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_statusLabel.Text = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
WriteBackSpellsList();
|
||||
WriteBackItemsList();
|
||||
ClassBlocksStore.Save(_currentDocument);
|
||||
localSaved = true;
|
||||
SetDirty(false);
|
||||
@@ -3736,6 +4008,8 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
_spellsGrid.Rows.Clear();
|
||||
_itemsGrid.Rows.Clear();
|
||||
_itemsSearchBox.Clear();
|
||||
_itemsListGrid.Rows.Clear();
|
||||
_itemsListSearchBox.Clear();
|
||||
_spellsListGrid.Rows.Clear();
|
||||
_spellsListSearchBox.Clear();
|
||||
_groupAurasGrid.Rows.Clear();
|
||||
@@ -3792,6 +4066,36 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void HandleItemsListDeleteClick(object? sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
if (sender is not DataGridView grid
|
||||
|| e.RowIndex < 0
|
||||
|| e.ColumnIndex < 0
|
||||
|| grid.Columns[e.ColumnIndex].Name != "Delete")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var row = grid.Rows[e.RowIndex];
|
||||
if (row.IsNewRow || row.Tag is not ClassBlocksStore.ItemsListEntry entry)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentDocument is not null)
|
||||
{
|
||||
if (entry.OriginalItemId > 0)
|
||||
{
|
||||
_currentDocument.DeletedItemsListOriginalIds.Add(entry.OriginalItemId);
|
||||
}
|
||||
|
||||
_currentDocument.ItemsList.Remove(entry);
|
||||
}
|
||||
|
||||
grid.Rows.RemoveAt(e.RowIndex);
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private void MoveSelectedRow(DataGridView grid, int delta)
|
||||
{
|
||||
if (grid.CurrentRow is null || grid.CurrentRow.IsNewRow)
|
||||
|
||||
Reference in New Issue
Block a user