mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
更新 ClassBlocksStore 和 ClassConfigEditorControl,支持编辑 spellsList;添加增压层数字段到状态和 UI 组件
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using static Shigure.LuaLiteParser;
|
||||
|
||||
namespace Shigure;
|
||||
|
||||
/// <summary>
|
||||
/// 读写 Fuyutsui class/*.lua 中的 ClassBlocks(states/auras/spells/group),
|
||||
/// 同时读取 spellsList 供界面展示;保存时只替换 ClassBlocks 表字面量,保留文件其余内容。
|
||||
/// 同时读写 spellsList;保存时替换 ClassBlocks 表字面量,并原位更新 spellsList 中已编辑的条目。
|
||||
/// </summary>
|
||||
internal static class ClassBlocksStore
|
||||
{
|
||||
@@ -38,6 +39,9 @@ internal static class ClassBlocksStore
|
||||
public long SpellId { get; set; }
|
||||
public int Index { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long OriginalSpellId { get; set; }
|
||||
public int OriginalIndex { get; set; }
|
||||
public string OriginalName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SpecBlocks
|
||||
@@ -164,7 +168,10 @@ internal static class ClassBlocksStore
|
||||
{
|
||||
SpellId = spellId,
|
||||
Index = (int)indexValue.Value,
|
||||
Name = name
|
||||
Name = name,
|
||||
OriginalSpellId = spellId,
|
||||
OriginalIndex = (int)indexValue.Value,
|
||||
OriginalName = name
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,10 +185,16 @@ internal static class ClassBlocksStore
|
||||
throw new InvalidOperationException("当前文件仍是旧版稀疏索引 ClassBlocks,无法用图形编辑器保存。");
|
||||
}
|
||||
|
||||
var updated = UpdateSpellsListEntries(document.SourceText, document.SpellsList);
|
||||
if (!TryExtractAssignedTable(updated, AssignmentName, out _, out var classBlocksStart, out var classBlocksEnd))
|
||||
{
|
||||
throw new InvalidOperationException("保存前无法重新定位 ClassBlocks 表。");
|
||||
}
|
||||
|
||||
var serialized = SerializeClassBlocks(document.Specs);
|
||||
var updated = document.SourceText[..document.TableStart]
|
||||
updated = updated[..classBlocksStart]
|
||||
+ serialized
|
||||
+ document.SourceText[document.TableEndExclusive..];
|
||||
+ updated[classBlocksEnd..];
|
||||
File.WriteAllText(document.FilePath, updated, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
if (!TryExtractAssignedTable(updated, AssignmentName, out _, out var start, out var end))
|
||||
@@ -192,6 +205,108 @@ internal static class ClassBlocksStore
|
||||
document.SourceText = updated;
|
||||
document.TableStart = start;
|
||||
document.TableEndExclusive = end;
|
||||
foreach (var spell in document.SpellsList)
|
||||
{
|
||||
spell.OriginalSpellId = spell.SpellId;
|
||||
spell.OriginalIndex = spell.Index;
|
||||
spell.OriginalName = spell.Name;
|
||||
}
|
||||
}
|
||||
|
||||
private static string UpdateSpellsListEntries(string source, IReadOnlyList<SpellsListEntry> entries)
|
||||
{
|
||||
var newEntries = entries.Where(entry => entry.OriginalSpellId == 0).ToArray();
|
||||
var changedEntries = entries
|
||||
.Where(entry => entry.OriginalSpellId != 0
|
||||
&& (entry.SpellId != entry.OriginalSpellId
|
||||
|| entry.Index != entry.OriginalIndex
|
||||
|| !string.Equals(entry.Name, entry.OriginalName, StringComparison.Ordinal)))
|
||||
.ToDictionary(entry => entry.OriginalSpellId);
|
||||
if (changedEntries.Count == 0 && newEntries.Length == 0)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
if (!TryExtractAssignedTable(source, SpellsListAssignmentName, out _, out var tableStart, out var tableEnd))
|
||||
{
|
||||
throw new InvalidOperationException($"当前文件中未找到 {SpellsListAssignmentName},无法保存技能列表。");
|
||||
}
|
||||
|
||||
var tableText = source[tableStart..tableEnd];
|
||||
var updatedOriginalIds = new HashSet<long>();
|
||||
var pattern = new Regex(
|
||||
"""^(?<prefix>[ \t]*\[[ \t]*)(?<spellId>\d+)(?<beforeIndex>[ \t]*\][ \t]*=[ \t]*\{[ \t]*index[ \t]*=[ \t]*)(?<index>\d+)(?<beforeName>[ \t]*,[ \t]*name[ \t]*=[ \t]*)(?<name>"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')(?<suffix>[^\n]*)$""",
|
||||
RegexOptions.Multiline | RegexOptions.CultureInvariant);
|
||||
|
||||
var updatedTable = pattern.Replace(tableText, match =>
|
||||
{
|
||||
if (!long.TryParse(match.Groups["spellId"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var originalSpellId)
|
||||
|| !changedEntries.TryGetValue(originalSpellId, out var entry))
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
updatedOriginalIds.Add(originalSpellId);
|
||||
var quotedName = match.Groups["name"].Value;
|
||||
var quote = quotedName[0];
|
||||
return match.Groups["prefix"].Value
|
||||
+ entry.SpellId.ToString(CultureInfo.InvariantCulture)
|
||||
+ match.Groups["beforeIndex"].Value
|
||||
+ entry.Index.ToString(CultureInfo.InvariantCulture)
|
||||
+ match.Groups["beforeName"].Value
|
||||
+ quote
|
||||
+ EscapeLuaString(entry.Name, quote)
|
||||
+ quote
|
||||
+ match.Groups["suffix"].Value;
|
||||
});
|
||||
|
||||
var missing = changedEntries.Keys.Where(id => !updatedOriginalIds.Contains(id)).ToArray();
|
||||
if (missing.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"无法在 {SpellsListAssignmentName} 中定位法术 ID {string.Join(", ", missing)} 的原始条目。");
|
||||
}
|
||||
|
||||
if (newEntries.Length > 0)
|
||||
{
|
||||
var newline = source.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
|
||||
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.Index))
|
||||
{
|
||||
insertion.Append(" [")
|
||||
.Append(entry.SpellId.ToString(CultureInfo.InvariantCulture))
|
||||
.Append("] = { index = ")
|
||||
.Append(entry.Index.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 EscapeLuaString(string value, char quote)
|
||||
{
|
||||
var escaped = value
|
||||
.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||
.Replace("\r", "\\r", StringComparison.Ordinal)
|
||||
.Replace("\n", "\\n", StringComparison.Ordinal)
|
||||
.Replace("\t", "\\t", StringComparison.Ordinal);
|
||||
return quote == '\''
|
||||
? escaped.Replace("'", "\\'", StringComparison.Ordinal)
|
||||
: escaped.Replace("\"", "\\\"", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static SpecBlocks ParseSpec(TableValue spec, out bool isModern)
|
||||
|
||||
@@ -46,7 +46,7 @@ internal static class ClassStateCatalog
|
||||
[
|
||||
"法力值", "怒气值", "集中值", "能量值", "符文", "符文能量",
|
||||
"星界能量", "漩涡值", "狂乱值", "奥术充能", "恶魔之怒", "痛苦值",
|
||||
"连击点", "神圣能量", "精华能量", "灵魂碎片", "真气"
|
||||
"连击点", "神圣能量", "精华能量", "灵魂碎片", "真气", "增压层数"
|
||||
]),
|
||||
(CategoryTarget,
|
||||
[
|
||||
|
||||
@@ -44,7 +44,7 @@ Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyut
|
||||
|
||||
- 置顶浮动条:显示程序名、当前职业图标颜色和逻辑状态,提供 `开启/关闭`、`设置`、`✕` 按钮。窗口可拖动和缩放,显示后自动启动运行循环。
|
||||
- `通用`:设置触发键和发送模式;从项目 Fuyutsui 更新配置并同步游戏插件;按实时环境筛选、自动选择或手动指定模块。
|
||||
- `配置`:直接编辑项目 `Fuyutsui/class/*.lua` 中的 `ClassBlocks`,包括状态、光环、法术和队伍字段。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/group` 格式。
|
||||
- `配置`:直接编辑项目 `Fuyutsui/class/*.lua` 中的 `ClassBlocks`,包括状态、光环、法术和队伍字段;技能列表页可编辑 `Fuyutsui.spellsList` 中索引 1–100 的法术 ID、索引和名称,也可输入 spellId 与名称并自动分配空闲索引。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/group` 格式。
|
||||
- `宏`:编辑项目 `Fuyutsui/core/classmacros.lua` 中各职业的动态宏、静态宏和特殊宏。动态宏每项占用 30 个团队点名槽位。
|
||||
- `模块`:新建、编辑、删除本地模块,维护作者、推荐天赋、匹配条件、动态字段和有序规则。规则支持拖拽、上移、下移、复制与插入。
|
||||
- `状态`:分栏显示基础状态、`auras`、`spells` 和模块计算出的动态单位/数值。
|
||||
|
||||
@@ -88,7 +88,7 @@ verified_at: 2026-08-10
|
||||
- 保存时只替换源文件中该 table literal,表外文本保留;表内部按 Store 支持的 schema 重新序列化,不承诺保留未知字段或原始格式。
|
||||
- 保存是直接写回源 Lua,不是临时文件原子替换,也不自动备份。
|
||||
- 旧稀疏专精会返回空编辑数据;若整个文档不是 modern,Store 拒绝保存。混合 modern/legacy 文件尤其危险:全局可被判 modern,但 legacy 专精仍为空,保存可能造成数据损失。
|
||||
- `spellsList` 在 ClassBlocks UI 中只读。
|
||||
- `spellsList` 在 ClassBlocks UI 中可编辑索引 1–100 的法术 ID、索引和名称;保存时原位更新对应 Lua 条目,保留未显示的索引 101+ 条目与表内注释。
|
||||
|
||||
## ClassBlocks → config
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ verified_at: 2026-08-10
|
||||
- 模块编辑器覆盖模块、Match、规则、动态单位、数量和数值调整,并支持排序。
|
||||
- 模块执行器支持规则 `Hotkey` 和 `Step`,但 `ModuleEditorControl.ReadRules` 保存时强制把二者写成空字符串。手工 JSON 中的非空值经 UI 打开再保存会丢失。
|
||||
- 模块根级 `Enabled` 可被 UI/JSON保存,但运行时忽略它;见模块页。
|
||||
- ClassBlocks 编辑器恢复前三个固定状态名为锚点/职业/专精;`spellsList` 只读。
|
||||
- ClassBlocks 编辑器恢复前三个固定状态名为锚点/职业/专精;`spellsList` 可编辑索引 1–100 的条目并原位回写 Lua。
|
||||
- 旧稀疏 ClassBlocks 不是可靠的只读预览:旧专精可显示为空,Store 会拒绝非 modern 文档保存;混合格式需特别谨慎。
|
||||
- ClassMacros 和 ClassBlocks 保存都会 canonical 重写目标表内部,未知字段/排版不保证保留。
|
||||
- 条件编辑器只能表达当前简单 AND/OR 语言,不支持括号;动态名称禁止空、`.`、`$`、纯数字和冲突名称。
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Shigure;
|
||||
|
||||
/// <summary>
|
||||
/// 图形化编辑 Fuyutsui class/*.lua 的 ClassBlocks(states / auras / spells / group),
|
||||
/// 并展示同文件中的 spellsList。
|
||||
/// 并编辑同文件中的 spellsList。
|
||||
/// </summary>
|
||||
public sealed class ClassConfigEditorControl : UserControl
|
||||
{
|
||||
@@ -25,6 +25,8 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private readonly DataGridView _aurasGrid = new();
|
||||
private readonly DataGridView _spellsGrid = new();
|
||||
private readonly DataGridView _spellsListGrid = new();
|
||||
private readonly TextBox _newSpellIdBox = new();
|
||||
private readonly TextBox _newSpellNameBox = new();
|
||||
private readonly NumericUpDown _groupNumBox = new();
|
||||
private readonly NumericUpDown _groupHealthBox = new();
|
||||
private readonly NumericUpDown _groupRoleBox = new();
|
||||
@@ -594,20 +596,92 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
BackColor = UiTheme.SurfaceRaised
|
||||
RowCount = 3,
|
||||
BackColor = UiTheme.SurfaceRaised,
|
||||
Margin = new Padding(0),
|
||||
Padding = new Padding(0)
|
||||
};
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 68));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
var hint = CreateFieldCaption("来自当前职业 Lua 的 Fuyutsui.spellsList,仅显示索引 1–100(只读)。");
|
||||
var addCard = new UiCardPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 4,
|
||||
RowCount = 1,
|
||||
Margin = new Padding(0, 0, 0, 10),
|
||||
Padding = new Padding(12, 8, 12, 8)
|
||||
};
|
||||
addCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
|
||||
addCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220));
|
||||
addCard.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||
addCard.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 96));
|
||||
addCard.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||
|
||||
addCard.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(_newSpellIdBox);
|
||||
_newSpellIdBox.Dock = DockStyle.None;
|
||||
_newSpellIdBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_newSpellIdBox.Margin = new Padding(0, 0, 8, 0);
|
||||
_newSpellIdBox.Height = 30;
|
||||
_newSpellIdBox.PlaceholderText = "spellId";
|
||||
_newSpellIdBox.MaxLength = 20;
|
||||
addCard.Controls.Add(_newSpellIdBox, 1, 0);
|
||||
|
||||
UiTheme.StyleTextBox(_newSpellNameBox);
|
||||
_newSpellNameBox.Dock = DockStyle.None;
|
||||
_newSpellNameBox.Anchor = AnchorStyles.Left | AnchorStyles.Right;
|
||||
_newSpellNameBox.Margin = new Padding(0, 0, 8, 0);
|
||||
_newSpellNameBox.Height = 30;
|
||||
_newSpellNameBox.PlaceholderText = "名称";
|
||||
addCard.Controls.Add(_newSpellNameBox, 2, 0);
|
||||
|
||||
var addControl = new UiPillTab("添加")
|
||||
{
|
||||
Selected = true,
|
||||
Dock = DockStyle.None,
|
||||
Anchor = AnchorStyles.Left | AnchorStyles.Right,
|
||||
Margin = new Padding(0),
|
||||
Height = 30,
|
||||
MinimumSize = new Size(88, 30)
|
||||
};
|
||||
addControl.Click += (_, _) => AddSpellsListEntry();
|
||||
addCard.Controls.Add(addControl, 3, 0);
|
||||
|
||||
void HandleAddOnEnter(object? _, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode != Keys.Enter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.SuppressKeyPress = true;
|
||||
AddSpellsListEntry();
|
||||
}
|
||||
|
||||
_newSpellIdBox.KeyDown += HandleAddOnEnter;
|
||||
_newSpellNameBox.KeyDown += HandleAddOnEnter;
|
||||
panel.Controls.Add(addCard, 0, 0);
|
||||
|
||||
var hint = CreateFieldCaption("来自当前职业 Lua 的 Fuyutsui.spellsList,仅编辑索引 1–100;保存后同步修改 Lua 对应条目。");
|
||||
hint.Padding = new Padding(8, 0, 0, 0);
|
||||
panel.Controls.Add(hint, 0, 0);
|
||||
panel.Controls.Add(hint, 0, 1);
|
||||
|
||||
ConfigureGrid(_spellsListGrid);
|
||||
_spellsListGrid.AllowUserToAddRows = false;
|
||||
_spellsListGrid.ReadOnly = true;
|
||||
_spellsListGrid.EditMode = DataGridViewEditMode.EditProgrammatically;
|
||||
_spellsListGrid.CellValueChanged += (_, _) => MarkDirty();
|
||||
_spellsListGrid.Columns.Add(new DataGridViewTextBoxColumn
|
||||
{
|
||||
Name = "SpellId",
|
||||
@@ -629,7 +703,7 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
|
||||
SortMode = DataGridViewColumnSortMode.NotSortable
|
||||
});
|
||||
panel.Controls.Add(_spellsListGrid, 0, 1);
|
||||
panel.Controls.Add(_spellsListGrid, 0, 2);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -1127,6 +1201,15 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
}
|
||||
|
||||
SelectSpec(_specList.SelectedItem as SpecOption);
|
||||
_suppressUi = true;
|
||||
try
|
||||
{
|
||||
FillSpellsListGrid();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressUi = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildSpecList(IReadOnlyList<SpecOption> options)
|
||||
@@ -1186,7 +1269,6 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
FillAurasGrid();
|
||||
FillSpellsGrid();
|
||||
FillGroupEditors();
|
||||
FillSpellsListGrid();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1390,6 +1472,8 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
private void FillSpellsListGrid()
|
||||
{
|
||||
_spellsListGrid.Rows.Clear();
|
||||
_newSpellIdBox.Clear();
|
||||
_newSpellNameBox.Clear();
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return;
|
||||
@@ -1397,10 +1481,11 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
|
||||
foreach (var spell in _currentDocument.SpellsList.Where(spell => spell.Index is >= 1 and <= 100))
|
||||
{
|
||||
_spellsListGrid.Rows.Add(
|
||||
var rowIndex = _spellsListGrid.Rows.Add(
|
||||
spell.SpellId.ToString(CultureInfo.InvariantCulture),
|
||||
spell.Index.ToString(CultureInfo.InvariantCulture),
|
||||
spell.Name);
|
||||
_spellsListGrid.Rows[rowIndex].Tag = spell;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1491,6 +1576,173 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
WriteBackGroup();
|
||||
}
|
||||
|
||||
private void WriteBackSpellsList()
|
||||
{
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (DataGridViewRow row in _spellsListGrid.Rows)
|
||||
{
|
||||
if (row.Tag is not ClassBlocksStore.SpellsListEntry entry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.SpellId = long.Parse(
|
||||
row.Cells["SpellId"].Value?.ToString()?.Trim() ?? "",
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture);
|
||||
entry.Index = int.Parse(
|
||||
row.Cells["Index"].Value?.ToString()?.Trim() ?? "",
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture);
|
||||
entry.Name = row.Cells["Name"].Value?.ToString()?.Trim() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSpellsListEntry()
|
||||
{
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
MessageBox.Show("请先选择一个职业文件。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_currentDocument.IsModernFormat)
|
||||
{
|
||||
MessageBox.Show("旧版稀疏索引格式暂不支持添加技能。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_spellsListGrid.EndEdit();
|
||||
if (!TryValidateSpellsList(out var validationError))
|
||||
{
|
||||
MessageBox.Show(validationError, "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!long.TryParse(_newSpellIdBox.Text.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var spellId)
|
||||
|| spellId <= 0)
|
||||
{
|
||||
MessageBox.Show("spellId 必须是正整数。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_newSpellIdBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var name = _newSpellNameBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
MessageBox.Show("名称不能为空。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_newSpellNameBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_currentDocument.SpellsList.Any(entry => entry.SpellId == spellId)
|
||||
|| _spellsListGrid.Rows.Cast<DataGridViewRow>().Any(row =>
|
||||
long.TryParse(row.Cells["SpellId"].Value?.ToString(), NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var existingSpellId)
|
||||
&& existingSpellId == spellId))
|
||||
{
|
||||
MessageBox.Show($"法术 ID {spellId} 已存在。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_newSpellIdBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var usedIndices = _spellsListGrid.Rows
|
||||
.Cast<DataGridViewRow>()
|
||||
.Select(row => int.TryParse(row.Cells["Index"].Value?.ToString(), NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var value) ? value : 0)
|
||||
.Where(value => value is >= 1 and <= 100)
|
||||
.ToHashSet();
|
||||
var nextIndex = Enumerable.Range(1, 100).FirstOrDefault(index => !usedIndices.Contains(index));
|
||||
if (nextIndex == 0)
|
||||
{
|
||||
MessageBox.Show("索引 1–100 已全部使用,无法继续添加技能。", "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new ClassBlocksStore.SpellsListEntry
|
||||
{
|
||||
SpellId = spellId,
|
||||
Index = nextIndex,
|
||||
Name = name
|
||||
};
|
||||
_currentDocument.SpellsList.Add(entry);
|
||||
|
||||
var rowIndex = _spellsListGrid.Rows.Add(
|
||||
spellId.ToString(CultureInfo.InvariantCulture),
|
||||
nextIndex.ToString(CultureInfo.InvariantCulture),
|
||||
name);
|
||||
var row = _spellsListGrid.Rows[rowIndex];
|
||||
row.Tag = entry;
|
||||
_spellsListGrid.ClearSelection();
|
||||
row.Selected = true;
|
||||
_spellsListGrid.CurrentCell = row.Cells["SpellId"];
|
||||
_spellsListGrid.FirstDisplayedScrollingRowIndex = rowIndex;
|
||||
|
||||
_newSpellIdBox.Clear();
|
||||
_newSpellNameBox.Clear();
|
||||
_newSpellIdBox.Focus();
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private bool TryValidateSpellsList(out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (_currentDocument is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var editedIds = new HashSet<long>();
|
||||
foreach (DataGridViewRow row in _spellsListGrid.Rows)
|
||||
{
|
||||
var rowNumber = row.Index + 1;
|
||||
if (!long.TryParse(row.Cells["SpellId"].Value?.ToString()?.Trim(), NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var spellId)
|
||||
|| spellId <= 0)
|
||||
{
|
||||
error = $"技能列表第 {rowNumber} 行的法术 ID 必须是正整数。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(row.Cells["Index"].Value?.ToString()?.Trim(), NumberStyles.None,
|
||||
CultureInfo.InvariantCulture, out var index)
|
||||
|| index is < 1 or > 100)
|
||||
{
|
||||
error = $"技能列表第 {rowNumber} 行的索引必须是 1–100 的整数。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(row.Cells["Name"].Value?.ToString()))
|
||||
{
|
||||
error = $"技能列表第 {rowNumber} 行的名称不能为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!editedIds.Add(spellId))
|
||||
{
|
||||
error = $"技能列表中的法术 ID {spellId} 重复。";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenIds = _currentDocument.SpellsList
|
||||
.Where(entry => entry.Index is < 1 or > 100)
|
||||
.Select(entry => entry.SpellId)
|
||||
.ToHashSet();
|
||||
var conflictId = editedIds.FirstOrDefault(hiddenIds.Contains);
|
||||
if (conflictId != 0)
|
||||
{
|
||||
error = $"法术 ID {conflictId} 已被技能列表中索引 101+ 的条目使用。";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void WriteBackStatesCategory(string category)
|
||||
{
|
||||
if (_currentSpec is null)
|
||||
@@ -1705,8 +1957,17 @@ public sealed class ClassConfigEditorControl : UserControl
|
||||
var localSaved = false;
|
||||
try
|
||||
{
|
||||
_spellsListGrid.EndEdit();
|
||||
if (!TryValidateSpellsList(out var validationError))
|
||||
{
|
||||
MessageBox.Show(validationError, "技能列表", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
_statusLabel.Text = validationError;
|
||||
return;
|
||||
}
|
||||
|
||||
// 切换分类前把当前状态表写回。
|
||||
CommitCurrentSpecFromUi();
|
||||
WriteBackSpellsList();
|
||||
ClassBlocksStore.Save(_currentDocument);
|
||||
localSaved = true;
|
||||
SetDirty(false);
|
||||
|
||||
@@ -784,7 +784,7 @@ public sealed class StatusForm : Form
|
||||
[
|
||||
"法力值", "怒气值", "集中值", "能量值", "符文", "符文能量",
|
||||
"星界能量", "漩涡值", "狂乱值", "恶魔之怒", "痛苦值",
|
||||
"连击点", "神圣能量", "精华能量", "灵魂碎片", "真气"
|
||||
"连击点", "神圣能量", "精华能量", "灵魂碎片", "真气", "增压层数"
|
||||
],
|
||||
150), 1, 0);
|
||||
fields.Controls.Add(CreateAboutFieldCard(
|
||||
|
||||
Reference in New Issue
Block a user