mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-09 18:53:20 +08:00
1395 lines
46 KiB
C#
1395 lines
46 KiB
C#
using System.Drawing;
|
|
|
|
namespace Shigure;
|
|
|
|
public sealed class ModuleEditorControl : UserControl
|
|
{
|
|
private readonly ModuleStore _moduleStore;
|
|
private readonly Action _runtimeRestartRequested;
|
|
private readonly ConditionFieldCatalog _fieldCatalog;
|
|
private readonly KeymapCatalog _keymapCatalog;
|
|
private readonly ListBox _moduleList = new();
|
|
private readonly TextBox _nameBox = new();
|
|
private readonly CheckBox _enabledBox = new();
|
|
private readonly ComboBox _classBox = new();
|
|
private readonly ComboBox _specBox = new();
|
|
private readonly ComboBox _partyTypeBox = new();
|
|
private readonly ComboBox _heroTalentBox = new();
|
|
private readonly DataGridView _rulesGrid = new();
|
|
private readonly DataGridViewComboBoxColumn _spellColumn = new();
|
|
private readonly DataGridViewComboBoxColumn _unitColumn = new();
|
|
private readonly ListView _unitsList = new();
|
|
private readonly Label _pathLabel = new();
|
|
private List<ModuleDefinition> _modules = new();
|
|
private ModuleDefinition? _selectedModule;
|
|
// 当前编辑中模块的动态单位/数量字段(含未保存的新增), 供目标下拉与条件字段使用。
|
|
private readonly List<ModuleUnit> _units = new();
|
|
private readonly List<ModuleCountField> _counts = new();
|
|
// 程序化恢复列宽时置真, 避免 ColumnWidthChanged 把默认值回写覆盖用户保存的宽度。
|
|
private bool _suppressColumnSave;
|
|
private static readonly PartyTypeOption[] PartyTypeOptions =
|
|
[
|
|
new("任意 (*)", null),
|
|
new("单人 (0)", "0"),
|
|
new("团队 (1-40)", "1-40"),
|
|
new("队伍 (46)", "46")
|
|
];
|
|
private static readonly MatchOption[] ClassOptions = BuildClassOptions();
|
|
// 这三列固定宽度并缓存; "条件"列为 Fill, 不缓存。
|
|
private static readonly string[] FixedWidthColumns = ["Enabled", "Spell", "Unit"];
|
|
|
|
public ModuleEditorControl(ModuleStore moduleStore, Action runtimeRestartRequested, string baseDirectory)
|
|
{
|
|
_moduleStore = moduleStore;
|
|
_runtimeRestartRequested = runtimeRestartRequested;
|
|
_fieldCatalog = ConditionFieldCatalog.Load(baseDirectory);
|
|
_keymapCatalog = KeymapCatalog.Load(baseDirectory);
|
|
InitializeComponent();
|
|
LoadModules();
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
Dock = DockStyle.Fill;
|
|
BackColor = UiTheme.Surface;
|
|
ForeColor = UiTheme.Text;
|
|
Font = new Font("Microsoft YaHei UI", 9F, FontStyle.Regular, GraphicsUnit.Point);
|
|
|
|
var root = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
ColumnCount = 2,
|
|
RowCount = 1,
|
|
Margin = new Padding(0)
|
|
};
|
|
root.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 230));
|
|
root.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
Controls.Add(root);
|
|
|
|
root.Controls.Add(BuildSidebar(), 0, 0);
|
|
root.Controls.Add(BuildEditor(), 1, 0);
|
|
}
|
|
|
|
private Control BuildSidebar()
|
|
{
|
|
var sidebar = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Background,
|
|
Padding = new Padding(0, 0, 10, 0),
|
|
ColumnCount = 1,
|
|
RowCount = 2
|
|
};
|
|
sidebar.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
sidebar.RowStyles.Add(new RowStyle(SizeType.Absolute, 38));
|
|
|
|
_moduleList.Dock = DockStyle.Fill;
|
|
_moduleList.BackColor = UiTheme.Background;
|
|
_moduleList.ForeColor = UiTheme.Text;
|
|
_moduleList.BorderStyle = BorderStyle.None;
|
|
_moduleList.IntegralHeight = false;
|
|
_moduleList.SelectedIndexChanged += (_, _) => SelectModule(_moduleList.SelectedIndex);
|
|
sidebar.Controls.Add(_moduleList, 0, 0);
|
|
|
|
var buttons = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.LeftToRight,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Background,
|
|
Margin = new Padding(0)
|
|
};
|
|
|
|
var addButton = UiTheme.CreateButton("新建", UiTheme.Field, UiTheme.Text);
|
|
addButton.Width = 72;
|
|
addButton.Height = 30;
|
|
addButton.Click += (_, _) => AddModule();
|
|
|
|
var reloadButton = UiTheme.CreateButton("刷新", UiTheme.Field, UiTheme.Text);
|
|
reloadButton.Width = 72;
|
|
reloadButton.Height = 30;
|
|
reloadButton.Click += (_, _) => LoadModules();
|
|
|
|
buttons.Controls.Add(addButton);
|
|
buttons.Controls.Add(reloadButton);
|
|
sidebar.Controls.Add(buttons, 0, 1);
|
|
|
|
return sidebar;
|
|
}
|
|
|
|
private Control BuildEditor()
|
|
{
|
|
var editor = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
Padding = new Padding(8, 0, 8, 4),
|
|
ColumnCount = 1,
|
|
RowCount = 5
|
|
};
|
|
editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 74));
|
|
editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 74));
|
|
editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 152));
|
|
editor.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
editor.RowStyles.Add(new RowStyle(SizeType.Absolute, 48));
|
|
|
|
editor.Controls.Add(BuildNameRow(), 0, 0);
|
|
editor.Controls.Add(BuildMatchRow(), 0, 1);
|
|
editor.Controls.Add(BuildUnitsPanel(), 0, 2);
|
|
editor.Controls.Add(BuildRulesGrid(), 0, 3);
|
|
editor.Controls.Add(BuildActionRow(), 0, 4);
|
|
return editor;
|
|
}
|
|
|
|
private Control BuildUnitsPanel()
|
|
{
|
|
var panel = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
ColumnCount = 2,
|
|
RowCount = 2,
|
|
Margin = new Padding(0, 2, 0, 4)
|
|
};
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 92));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 22));
|
|
panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
|
|
|
var title = new Label
|
|
{
|
|
Text = "动态单位 / 数量字段(可在“目标”和“条件”中引用)",
|
|
Dock = DockStyle.Fill,
|
|
ForeColor = UiTheme.Muted,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
AutoEllipsis = true
|
|
};
|
|
panel.Controls.Add(title, 0, 0);
|
|
panel.SetColumnSpan(title, 2);
|
|
|
|
foreach (var column in new[] { ("名称", 130), ("类型", 50), ("摘要", 240) })
|
|
{
|
|
_unitsList.Columns.Add(column.Item1, column.Item2);
|
|
}
|
|
|
|
_unitsList.Dock = DockStyle.Fill;
|
|
_unitsList.View = View.Details;
|
|
_unitsList.FullRowSelect = true;
|
|
_unitsList.MultiSelect = false;
|
|
_unitsList.HideSelection = false;
|
|
_unitsList.GridLines = false;
|
|
_unitsList.HeaderStyle = ColumnHeaderStyle.Nonclickable;
|
|
_unitsList.BackColor = UiTheme.Field;
|
|
_unitsList.ForeColor = UiTheme.Text;
|
|
_unitsList.BorderStyle = BorderStyle.None;
|
|
_unitsList.DoubleClick += (_, _) => EditSelectedUnit();
|
|
panel.Controls.Add(_unitsList, 0, 1);
|
|
|
|
var buttons = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.TopDown,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Surface,
|
|
Margin = new Padding(6, 0, 0, 0),
|
|
Padding = new Padding(0)
|
|
};
|
|
buttons.Resize += (_, _) => LayoutUnitActionButtons(buttons);
|
|
|
|
var addButton = CreateUnitActionButton("添加", UiTheme.Field, UiTheme.Text, bottomGap: true);
|
|
addButton.Click += (_, _) => AddUnit();
|
|
|
|
var editButton = CreateUnitActionButton("编辑", UiTheme.Field, UiTheme.Text, bottomGap: true);
|
|
editButton.Click += (_, _) => EditSelectedUnit();
|
|
|
|
var deleteButton = CreateUnitActionButton("删除", UiTheme.Field, UiTheme.Danger, bottomGap: false);
|
|
deleteButton.Click += (_, _) => DeleteSelectedUnit();
|
|
|
|
buttons.Controls.Add(addButton);
|
|
buttons.Controls.Add(editButton);
|
|
buttons.Controls.Add(deleteButton);
|
|
panel.Controls.Add(buttons, 1, 1);
|
|
|
|
return panel;
|
|
}
|
|
|
|
private Control BuildNameRow()
|
|
{
|
|
var row = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
ColumnCount = 4,
|
|
RowCount = 2,
|
|
Margin = new Padding(0)
|
|
};
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 58));
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
|
|
row.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
|
|
row.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
|
|
|
|
row.Controls.Add(CreateLabel("名称"), 0, 0);
|
|
StyleTextBox(_nameBox);
|
|
_nameBox.Dock = DockStyle.Fill;
|
|
row.Controls.Add(_nameBox, 1, 0);
|
|
|
|
_enabledBox.Text = "启用";
|
|
_enabledBox.Checked = true;
|
|
_enabledBox.Dock = DockStyle.Fill;
|
|
_enabledBox.ForeColor = UiTheme.Text;
|
|
_enabledBox.BackColor = UiTheme.Surface;
|
|
row.Controls.Add(_enabledBox, 2, 0);
|
|
|
|
_pathLabel.Dock = DockStyle.Fill;
|
|
_pathLabel.ForeColor = UiTheme.Muted;
|
|
_pathLabel.TextAlign = ContentAlignment.MiddleLeft;
|
|
_pathLabel.AutoEllipsis = true;
|
|
row.Controls.Add(_pathLabel, 0, 1);
|
|
row.SetColumnSpan(_pathLabel, 4);
|
|
|
|
return row;
|
|
}
|
|
|
|
private Control BuildMatchRow()
|
|
{
|
|
var matchLabels = new[] { "职业", "专精", "英雄天赋", "队伍类型" };
|
|
|
|
var row = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
ColumnCount = 8,
|
|
RowCount = 2,
|
|
Margin = new Padding(0)
|
|
};
|
|
foreach (var label in matchLabels)
|
|
{
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, MeasureLabelColumnWidth(label, Font)));
|
|
row.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25));
|
|
}
|
|
|
|
ResetClassOptions(_classBox);
|
|
ResetSpecOptions(_specBox, null);
|
|
ResetHeroTalentOptions(_heroTalentBox, null, null);
|
|
_classBox.SelectedIndexChanged += (_, _) =>
|
|
{
|
|
ResetSpecOptions(_specBox, ReadMatchCombo(_classBox));
|
|
ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox));
|
|
RefreshKeymapColumns();
|
|
};
|
|
_specBox.SelectedIndexChanged += (_, _) =>
|
|
ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox));
|
|
|
|
AddMatchField(row, "职业:", _classBox, 0);
|
|
AddMatchField(row, "专精:", _specBox, 2);
|
|
AddMatchField(row, "英雄天赋:", _heroTalentBox, 4);
|
|
AddMatchField(row, "队伍类型:", _partyTypeBox, 6);
|
|
return row;
|
|
}
|
|
|
|
private Control BuildRulesGrid()
|
|
{
|
|
_rulesGrid.Dock = DockStyle.Fill;
|
|
_rulesGrid.Margin = new Padding(0);
|
|
_rulesGrid.BackgroundColor = UiTheme.Surface;
|
|
_rulesGrid.BorderStyle = BorderStyle.None;
|
|
_rulesGrid.GridColor = UiTheme.Field;
|
|
_rulesGrid.EnableHeadersVisualStyles = false;
|
|
_rulesGrid.ColumnHeadersDefaultCellStyle.BackColor = UiTheme.Field;
|
|
_rulesGrid.ColumnHeadersDefaultCellStyle.ForeColor = UiTheme.Muted;
|
|
_rulesGrid.ColumnHeadersDefaultCellStyle.SelectionBackColor = UiTheme.Field;
|
|
_rulesGrid.DefaultCellStyle.BackColor = UiTheme.Surface;
|
|
_rulesGrid.DefaultCellStyle.ForeColor = UiTheme.Text;
|
|
_rulesGrid.DefaultCellStyle.SelectionBackColor = UiTheme.Hover;
|
|
_rulesGrid.DefaultCellStyle.SelectionForeColor = UiTheme.Text;
|
|
_rulesGrid.RowHeadersVisible = false;
|
|
_rulesGrid.AllowUserToAddRows = true;
|
|
_rulesGrid.AllowUserToDeleteRows = true;
|
|
_rulesGrid.AllowUserToResizeColumns = true;
|
|
_rulesGrid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
|
|
|
// 启用/技能/目标 三列宽度固定可调并缓存; 条件列用 Fill 自动充满剩余窗口。
|
|
_rulesGrid.Columns.Add(new DataGridViewCheckBoxColumn
|
|
{
|
|
Name = "Enabled",
|
|
HeaderText = "启用",
|
|
Width = 100,
|
|
AutoSizeMode = DataGridViewAutoSizeColumnMode.None
|
|
});
|
|
_spellColumn.Name = "Spell";
|
|
_spellColumn.HeaderText = "技能";
|
|
_spellColumn.Width = 150;
|
|
_spellColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
|
|
_spellColumn.FlatStyle = FlatStyle.Flat;
|
|
_rulesGrid.Columns.Add(_spellColumn);
|
|
_unitColumn.Name = "Unit";
|
|
_unitColumn.HeaderText = "目标";
|
|
_unitColumn.Width = 150;
|
|
_unitColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
|
|
_unitColumn.FlatStyle = FlatStyle.Flat;
|
|
_rulesGrid.Columns.Add(_unitColumn);
|
|
_rulesGrid.Columns.Add(new DataGridViewTextBoxColumn
|
|
{
|
|
Name = "Condition",
|
|
HeaderText = "条件 (点击编辑)",
|
|
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
|
|
ReadOnly = true
|
|
});
|
|
_rulesGrid.Columns.Add(new DataGridViewButtonColumn
|
|
{
|
|
Name = "Delete",
|
|
HeaderText = string.Empty,
|
|
Text = "删除",
|
|
UseColumnTextForButtonValue = true,
|
|
Width = 56,
|
|
AutoSizeMode = DataGridViewAutoSizeColumnMode.None,
|
|
FlatStyle = FlatStyle.Flat
|
|
});
|
|
_rulesGrid.CellClick += OnRulesGridCellClick;
|
|
_rulesGrid.DataError += (_, e) => e.ThrowException = false;
|
|
_rulesGrid.ColumnWidthChanged += OnColumnWidthChanged;
|
|
_rulesGrid.CellValueChanged += OnRulesGridCellValueChanged;
|
|
// 组合框改值默认要等失焦才提交; 立即提交以便"目标"随"技能"实时联动。
|
|
_rulesGrid.CurrentCellDirtyStateChanged += (_, _) =>
|
|
{
|
|
if (_rulesGrid.IsCurrentCellDirty && _rulesGrid.CurrentCell is DataGridViewComboBoxCell)
|
|
{
|
|
_rulesGrid.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
|
}
|
|
};
|
|
RefreshKeymapColumns();
|
|
ApplyColumnWidths(UiCacheStore.Load().ModuleRulesGridColumns);
|
|
|
|
return _rulesGrid;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 按当前选中职业的 keymap 重建“技能/目标”下拉选项。
|
|
/// 技能去重(同名技能只出现一次), unit 去重升序; 首项留空表示不填。
|
|
/// 已有行里不在 keymap 中的旧值会补录为额外选项, 避免数据丢失。
|
|
/// </summary>
|
|
private void RefreshKeymapColumns()
|
|
{
|
|
var classId = ReadMatchCombo(_classBox);
|
|
|
|
_spellColumn.Items.Clear();
|
|
_spellColumn.Items.Add(string.Empty);
|
|
foreach (var spell in _keymapCatalog.GetSpells(classId))
|
|
{
|
|
_spellColumn.Items.Add(spell);
|
|
}
|
|
|
|
// 列级 unit 选项作为新行(尚未选技能)的默认全集; 已有行用单元格级选项按技能联动。
|
|
_unitColumn.Items.Clear();
|
|
_unitColumn.Items.Add(string.Empty);
|
|
foreach (var unit in _keymapCatalog.GetUnits(classId))
|
|
{
|
|
_unitColumn.Items.Add(unit.ToString());
|
|
}
|
|
|
|
foreach (DataGridViewRow row in _rulesGrid.Rows)
|
|
{
|
|
if (row.IsNewRow)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EnsureComboItem(_spellColumn, row.Cells["Spell"].Value);
|
|
UpdateUnitCellItems(row);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 按该行当前选中的技能, 把"目标"单元格的可选 unit 重建为该技能在 keymap 中实际配置过的值。
|
|
/// 旧值若不在新选项内则补录保留; 若是技能切换导致的非法值则清空。
|
|
/// </summary>
|
|
private void UpdateUnitCellItems(DataGridViewRow row)
|
|
{
|
|
if (row.IsNewRow || row.Cells["Unit"] is not DataGridViewComboBoxCell cell)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RebuildUnitCell(row, cell.Value?.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重建"目标"单元格选项并写入目标值。选项 = 当前技能在 keymap 中的 unit 集合。
|
|
/// desiredValue 合法则保留; 自定义技能(keymap 无该技能)保留旧值; 否则清空。
|
|
/// </summary>
|
|
private void RebuildUnitCell(DataGridViewRow row, string? desiredValue)
|
|
{
|
|
if (row.IsNewRow || row.Cells["Unit"] is not DataGridViewComboBoxCell cell)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var classId = ReadMatchCombo(_classBox);
|
|
var spell = row.Cells["Spell"].Value?.ToString();
|
|
var allowed = _keymapCatalog.GetUnitsForSpell(classId, spell);
|
|
|
|
cell.Items.Clear();
|
|
cell.Items.Add(string.Empty);
|
|
foreach (var unit in allowed)
|
|
{
|
|
cell.Items.Add(unit.ToString());
|
|
}
|
|
|
|
// 动态单位与技能无关, 始终可选; 放在 keymap 编号之后。
|
|
foreach (var unit in _units)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(unit.Name) && !cell.Items.Contains(unit.Name))
|
|
{
|
|
cell.Items.Add(unit.Name);
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(desiredValue))
|
|
{
|
|
cell.Value = string.Empty;
|
|
}
|
|
else if (cell.Items.Contains(desiredValue))
|
|
{
|
|
// keymap 编号或动态单位名(已在上面加入), 直接保留。
|
|
cell.Value = desiredValue;
|
|
}
|
|
else if (allowed.Count == 0)
|
|
{
|
|
// 该技能不在 keymap(自定义技能), 保留旧值不强制清空。
|
|
cell.Items.Add(desiredValue);
|
|
cell.Value = desiredValue;
|
|
}
|
|
else
|
|
{
|
|
// 技能切换导致旧的数字目标非法, 清空。
|
|
cell.Value = string.Empty;
|
|
}
|
|
}
|
|
|
|
private void OnRulesGridCellValueChanged(object? sender, DataGridViewCellEventArgs e)
|
|
{
|
|
if (e.RowIndex < 0 || e.ColumnIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 技能改变时联动刷新该行"目标"的可选值。
|
|
if (_rulesGrid.Columns[e.ColumnIndex].Name == "Spell")
|
|
{
|
|
UpdateUnitCellItems(_rulesGrid.Rows[e.RowIndex]);
|
|
}
|
|
}
|
|
|
|
private static void EnsureComboItem(DataGridViewComboBoxColumn column, object? value)
|
|
{
|
|
var text = value?.ToString();
|
|
if (!string.IsNullOrEmpty(text) && !column.Items.Contains(text))
|
|
{
|
|
column.Items.Add(text);
|
|
}
|
|
}
|
|
|
|
private void AddUnit()
|
|
{
|
|
using var editor = new UnitEditorForm(GetAuraFields(), CollectTakenNames(), null, null);
|
|
if (editor.ShowDialog(FindForm()) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (editor.ResultUnit is { } unit)
|
|
{
|
|
_units.Add(unit);
|
|
}
|
|
else if (editor.ResultCount is { } count)
|
|
{
|
|
_counts.Add(count);
|
|
}
|
|
|
|
RefreshUnitsList();
|
|
RefreshUnitDependentUi();
|
|
}
|
|
|
|
private void EditSelectedUnit()
|
|
{
|
|
var (kind, index) = GetSelectedUnitRef();
|
|
if (kind == UnitRowKind.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var existingUnit = kind == UnitRowKind.Unit ? _units[index] : null;
|
|
var existingCount = kind == UnitRowKind.Count ? _counts[index] : null;
|
|
var ownName = existingUnit?.Name ?? existingCount?.Name;
|
|
var ownHealthName = existingUnit?.HealthName;
|
|
|
|
using var editor = new UnitEditorForm(GetAuraFields(), CollectTakenNames(ownName, ownHealthName), existingUnit, existingCount);
|
|
if (editor.ShowDialog(FindForm()) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 类别可能在编辑中改变(单位↔数量), 先移除原项再按结果加入。
|
|
if (kind == UnitRowKind.Unit)
|
|
{
|
|
_units.RemoveAt(index);
|
|
}
|
|
else
|
|
{
|
|
_counts.RemoveAt(index);
|
|
}
|
|
|
|
if (editor.ResultUnit is { } unit)
|
|
{
|
|
_units.Add(unit);
|
|
}
|
|
else if (editor.ResultCount is { } count)
|
|
{
|
|
_counts.Add(count);
|
|
}
|
|
|
|
RefreshUnitsList();
|
|
RefreshUnitDependentUi();
|
|
}
|
|
|
|
private void DeleteSelectedUnit()
|
|
{
|
|
var (kind, index) = GetSelectedUnitRef();
|
|
if (kind == UnitRowKind.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (kind == UnitRowKind.Unit)
|
|
{
|
|
_units.RemoveAt(index);
|
|
}
|
|
else
|
|
{
|
|
_counts.RemoveAt(index);
|
|
}
|
|
|
|
RefreshUnitsList();
|
|
RefreshUnitDependentUi();
|
|
}
|
|
|
|
// ListView 行顺序: 先全部单位, 再全部数量。把选中行映射回对应列表索引。
|
|
private (UnitRowKind Kind, int Index) GetSelectedUnitRef()
|
|
{
|
|
if (_unitsList.SelectedIndices.Count == 0)
|
|
{
|
|
return (UnitRowKind.None, -1);
|
|
}
|
|
|
|
var row = _unitsList.SelectedIndices[0];
|
|
if (row < _units.Count)
|
|
{
|
|
return (UnitRowKind.Unit, row);
|
|
}
|
|
|
|
var countIndex = row - _units.Count;
|
|
return countIndex < _counts.Count ? (UnitRowKind.Count, countIndex) : (UnitRowKind.None, -1);
|
|
}
|
|
|
|
private void RefreshUnitsList()
|
|
{
|
|
_unitsList.BeginUpdate();
|
|
_unitsList.Items.Clear();
|
|
foreach (var unit in _units)
|
|
{
|
|
var name = string.IsNullOrWhiteSpace(unit.HealthName) ? unit.Name : $"{unit.Name} / {unit.HealthName}";
|
|
_unitsList.Items.Add(new ListViewItem([name, "单位", DescribeUnit(unit)]));
|
|
}
|
|
|
|
foreach (var count in _counts)
|
|
{
|
|
_unitsList.Items.Add(new ListViewItem([count.Name, "数量", DescribeCount(count)]));
|
|
}
|
|
|
|
_unitsList.EndUpdate();
|
|
}
|
|
|
|
// 单位/数量增删改后, 刷新各规则行"目标"下拉以反映最新的动态单位名。
|
|
private void RefreshUnitDependentUi()
|
|
{
|
|
foreach (DataGridViewRow row in _rulesGrid.Rows)
|
|
{
|
|
if (!row.IsNewRow)
|
|
{
|
|
UpdateUnitCellItems(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
private IReadOnlyList<string> GetAuraFields()
|
|
{
|
|
return _fieldCatalog
|
|
.GetGroupFields(ReadMatchCombo(_classBox), ReadMatchCombo(_specBox))
|
|
.Select(field => field.Name)
|
|
.ToList();
|
|
}
|
|
|
|
// 名称查重集合: 其它单位/数量(含生命值名) + 当前职业/专精的状态字段与 group 字段; 排除正在编辑项自身的名称。
|
|
private IReadOnlyCollection<string> CollectTakenNames(params string?[] ownNames)
|
|
{
|
|
var classId = ReadMatchCombo(_classBox);
|
|
var specId = ReadMatchCombo(_specBox);
|
|
var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var unit in _units)
|
|
{
|
|
taken.Add(unit.Name);
|
|
if (!string.IsNullOrWhiteSpace(unit.HealthName))
|
|
{
|
|
taken.Add(unit.HealthName);
|
|
}
|
|
}
|
|
|
|
foreach (var count in _counts)
|
|
{
|
|
taken.Add(count.Name);
|
|
}
|
|
|
|
foreach (var field in _fieldCatalog.GetFields(classId, specId))
|
|
{
|
|
taken.Add(field.Name);
|
|
}
|
|
|
|
foreach (var field in _fieldCatalog.GetGroupFields(classId, specId))
|
|
{
|
|
taken.Add(field.Name);
|
|
}
|
|
|
|
foreach (var ownName in ownNames)
|
|
{
|
|
if (!string.IsNullOrEmpty(ownName))
|
|
{
|
|
taken.Remove(ownName);
|
|
}
|
|
}
|
|
|
|
return taken;
|
|
}
|
|
|
|
private static string DescribeUnit(ModuleUnit unit)
|
|
{
|
|
var threshold = unit.HealthThreshold ?? 100;
|
|
var aura = unit.AuraNames is { Count: > 0 } ? unit.AuraNames[0] : "?";
|
|
var auras = unit.AuraNames is { Count: > 0 } ? string.Join("/", unit.AuraNames) : "?";
|
|
var dir = unit.Reverse ? "逆序" : "正序";
|
|
return unit.Kind switch
|
|
{
|
|
UnitSelectorKind.LowestHealth => $"血量最低 (<{threshold})",
|
|
UnitSelectorKind.LowestHealthWithAnyAura => $"带任一[{auras}]且血最低 (<{threshold})",
|
|
UnitSelectorKind.LowestHealthWithoutAura => $"不带[{aura}]且血最低 (<{threshold})",
|
|
UnitSelectorKind.LowestHealthWithAura => $"带[{aura}]且血最低 (<{threshold})",
|
|
UnitSelectorKind.LowestHealthWithAuraCount => $"[{aura}]={unit.AuraCount}且血最低 (<{threshold})",
|
|
UnitSelectorKind.UnitWithRole => $"职责={unit.Role} {dir}首个",
|
|
UnitSelectorKind.UnitWithRoleWithoutAura => $"职责={unit.Role}且不带[{aura}] {dir}",
|
|
UnitSelectorKind.UnitWithAura => $"带[{aura}] 持续最久",
|
|
UnitSelectorKind.UnitWithDispelType => $"驱散类型={unit.DispelType}",
|
|
_ => unit.Kind.ToString()
|
|
};
|
|
}
|
|
|
|
private static string DescribeCount(ModuleCountField count)
|
|
{
|
|
var threshold = count.HealthThreshold ?? 100;
|
|
return count.Kind switch
|
|
{
|
|
CountKind.UnitsBelowHealth => $"血量<{threshold} 的人数",
|
|
CountKind.UnitsWithoutAuraBelowHealth => $"不带[{count.AuraName}]且血<{threshold} 的人数",
|
|
CountKind.UnitsWithAura => $"带[{count.AuraName}] 的人数",
|
|
_ => count.Kind.ToString()
|
|
};
|
|
}
|
|
|
|
private enum UnitRowKind
|
|
{
|
|
None,
|
|
Unit,
|
|
Count
|
|
}
|
|
|
|
private void ApplyColumnWidths(Dictionary<string, int>? widths)
|
|
{
|
|
if (widths is null || widths.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_suppressColumnSave = true;
|
|
try
|
|
{
|
|
foreach (var name in FixedWidthColumns)
|
|
{
|
|
if (widths.TryGetValue(name, out var width) && width > 0)
|
|
{
|
|
_rulesGrid.Columns[name]!.Width = width;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_suppressColumnSave = false;
|
|
}
|
|
}
|
|
|
|
private void OnColumnWidthChanged(object? sender, DataGridViewColumnEventArgs e)
|
|
{
|
|
// Fill 列(条件)宽度随窗口/其它列变化, 不参与保存; 程序化恢复期间也跳过。
|
|
if (_suppressColumnSave || e.Column.Name == "Condition")
|
|
{
|
|
return;
|
|
}
|
|
|
|
SaveColumnWidths();
|
|
}
|
|
|
|
private void SaveColumnWidths()
|
|
{
|
|
var cache = UiCacheStore.Load();
|
|
cache.ModuleRulesGridColumns ??= new();
|
|
|
|
foreach (var name in FixedWidthColumns)
|
|
{
|
|
cache.ModuleRulesGridColumns[name] = _rulesGrid.Columns[name]!.Width;
|
|
}
|
|
|
|
UiCacheStore.Save(cache);
|
|
}
|
|
|
|
private void OnRulesGridCellClick(object? sender, DataGridViewCellEventArgs e)
|
|
{
|
|
if (e.RowIndex < 0 || e.ColumnIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var columnName = _rulesGrid.Columns[e.ColumnIndex].Name;
|
|
if (columnName == "Delete")
|
|
{
|
|
DeleteRule(e.RowIndex);
|
|
return;
|
|
}
|
|
|
|
if (columnName == "Condition")
|
|
{
|
|
OpenConditionEditor(e.RowIndex);
|
|
}
|
|
}
|
|
|
|
private void DeleteRule(int rowIndex)
|
|
{
|
|
var row = _rulesGrid.Rows[rowIndex];
|
|
// 新行占位符无需删除。
|
|
if (!row.IsNewRow)
|
|
{
|
|
_rulesGrid.Rows.RemoveAt(rowIndex);
|
|
}
|
|
}
|
|
|
|
private void OpenConditionEditor(int rowIndex)
|
|
{
|
|
var row = _rulesGrid.Rows[rowIndex];
|
|
var current = row.IsNewRow ? string.Empty : CellText(row, "Condition");
|
|
var fields = BuildConditionFields();
|
|
|
|
using var editor = new ConditionEditorForm(fields, current);
|
|
if (editor.ShowDialog(FindForm()) != DialogResult.OK)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (row.IsNewRow)
|
|
{
|
|
// 新行占位符不能直接赋值, 改为追加一行。
|
|
if (!string.IsNullOrWhiteSpace(editor.ConditionText))
|
|
{
|
|
_rulesGrid.Rows.Add(true, string.Empty, string.Empty, editor.ConditionText);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
row.Cells["Condition"].Value = editor.ConditionText;
|
|
}
|
|
|
|
// 条件字段 = 状态/技能字段 + 每个动态单位的 名字.字段 与裸名(存在) + 每个数量名。
|
|
private IReadOnlyList<ConditionField> BuildConditionFields()
|
|
{
|
|
var classId = ReadMatchCombo(_classBox);
|
|
var specId = ReadMatchCombo(_specBox);
|
|
var fields = new List<ConditionField>(_fieldCatalog.GetFields(classId, specId));
|
|
var groupFields = _fieldCatalog.GetGroupFields(classId, specId);
|
|
|
|
foreach (var unit in _units)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(unit.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var groupField in groupFields)
|
|
{
|
|
fields.Add(new ConditionField(
|
|
$"{unit.Name}.{groupField.Name}",
|
|
$"{unit.Name}: {groupField.DisplayName}",
|
|
groupField.Type));
|
|
}
|
|
|
|
// 裸单位名作为存在性布尔。
|
|
fields.Add(new ConditionField(unit.Name, $"{unit.Name} (存在)", ConditionFieldType.Bool));
|
|
|
|
// 名称2: 该单位 生命值 的直接命名数值字段。
|
|
if (!string.IsNullOrWhiteSpace(unit.HealthName))
|
|
{
|
|
fields.Add(new ConditionField(unit.HealthName, $"{unit.HealthName} (生命值)", ConditionFieldType.Int));
|
|
}
|
|
}
|
|
|
|
foreach (var count in _counts)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(count.Name))
|
|
{
|
|
fields.Add(new ConditionField(count.Name, $"人数: {count.Name}", ConditionFieldType.Int));
|
|
}
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
private Control BuildActionRow()
|
|
{
|
|
var row = new Panel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
BackColor = UiTheme.Surface,
|
|
Margin = new Padding(0),
|
|
Padding = new Padding(0)
|
|
};
|
|
|
|
var hint = new Label
|
|
{
|
|
Text = "目标可选 keymap 编号或上方定义的动态单位;点击“条件”列打开可视化编辑器",
|
|
Dock = DockStyle.Fill,
|
|
ForeColor = UiTheme.Muted,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
AutoEllipsis = true,
|
|
Margin = new Padding(0)
|
|
};
|
|
|
|
var buttons = new FlowLayoutPanel
|
|
{
|
|
AutoSize = true,
|
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
|
Dock = DockStyle.Right,
|
|
FlowDirection = FlowDirection.RightToLeft,
|
|
WrapContents = false,
|
|
BackColor = UiTheme.Surface,
|
|
Margin = new Padding(0),
|
|
Padding = new Padding(0, 6, 0, 6)
|
|
};
|
|
|
|
var saveButton = UiTheme.CreateButton("保存", UiTheme.Accent, Color.Black);
|
|
saveButton.Margin = new Padding(8, 0, 0, 0);
|
|
saveButton.Click += (_, _) => SaveSelectedModule();
|
|
|
|
var deleteButton = UiTheme.CreateButton("删除", UiTheme.Field, UiTheme.Danger);
|
|
deleteButton.Margin = new Padding(8, 0, 0, 0);
|
|
deleteButton.Click += (_, _) => DeleteSelectedModule();
|
|
|
|
buttons.Controls.Add(saveButton);
|
|
buttons.Controls.Add(deleteButton);
|
|
row.Controls.Add(hint);
|
|
row.Controls.Add(buttons);
|
|
return row;
|
|
}
|
|
|
|
private void LoadModules()
|
|
{
|
|
_moduleStore.Reload();
|
|
_modules = _moduleStore.GetModules().ToList();
|
|
_moduleList.Items.Clear();
|
|
foreach (var module in _modules)
|
|
{
|
|
_moduleList.Items.Add(FormatModuleListItem(module));
|
|
}
|
|
|
|
if (_modules.Count > 0)
|
|
{
|
|
_moduleList.SelectedIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
ClearEditor();
|
|
}
|
|
}
|
|
|
|
private void SelectModule(int index)
|
|
{
|
|
if (index < 0 || index >= _modules.Count)
|
|
{
|
|
ClearEditor();
|
|
return;
|
|
}
|
|
|
|
_selectedModule = _modules[index].Clone();
|
|
FillEditor(_selectedModule);
|
|
}
|
|
|
|
private void FillEditor(ModuleDefinition module)
|
|
{
|
|
_nameBox.Text = module.Name;
|
|
_enabledBox.Checked = module.Enabled;
|
|
// 先填充动态单位/数量, 后续目标下拉与条件字段都依赖它们。
|
|
_units.Clear();
|
|
_units.AddRange(module.Units.Select(unit => unit.Clone()));
|
|
_counts.Clear();
|
|
_counts.AddRange(module.Counts.Select(count => count.Clone()));
|
|
RefreshUnitsList();
|
|
SelectClass(module.Match.ClassId);
|
|
SelectSpec(module.Match.SpecId);
|
|
SelectPartyType(module.Match.PartyType);
|
|
SelectHeroTalent(module.Match.HeroTalent);
|
|
_pathLabel.Text = module.FilePath ?? "尚未保存";
|
|
_rulesGrid.Rows.Clear();
|
|
RefreshKeymapColumns();
|
|
ApplyColumnWidths(UiCacheStore.Load().ModuleRulesGridColumns);
|
|
|
|
foreach (var rule in module.Rules)
|
|
{
|
|
// 动态目标优先显示单位名, 否则显示数字单位。
|
|
var unitText = !string.IsNullOrWhiteSpace(rule.UnitName)
|
|
? rule.UnitName!
|
|
: rule.Unit?.ToString() ?? string.Empty;
|
|
EnsureComboItem(_spellColumn, rule.Spell);
|
|
// 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。
|
|
var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, rule.Condition);
|
|
RebuildUnitCell(_rulesGrid.Rows[index], unitText);
|
|
}
|
|
}
|
|
|
|
private void ClearEditor()
|
|
{
|
|
_selectedModule = null;
|
|
_nameBox.Clear();
|
|
_enabledBox.Checked = false;
|
|
_units.Clear();
|
|
_counts.Clear();
|
|
RefreshUnitsList();
|
|
SelectClass(null);
|
|
SelectSpec(null);
|
|
SelectPartyType(null);
|
|
SelectHeroTalent(null);
|
|
_pathLabel.Text = "无模块";
|
|
_rulesGrid.Rows.Clear();
|
|
}
|
|
|
|
private void AddModule()
|
|
{
|
|
var module = ModuleDefinition.CreateDefault(_moduleStore.CreateNextModuleName());
|
|
try
|
|
{
|
|
_moduleStore.Save(module);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
LoadModules();
|
|
var index = _modules.FindIndex(existing => string.Equals(existing.Id, module.Id, StringComparison.OrdinalIgnoreCase));
|
|
if (index >= 0)
|
|
{
|
|
_moduleList.SelectedIndex = index;
|
|
}
|
|
|
|
_runtimeRestartRequested();
|
|
}
|
|
|
|
private void SaveSelectedModule()
|
|
{
|
|
if (_selectedModule is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryReadModule(out var module))
|
|
{
|
|
return;
|
|
}
|
|
|
|
ModuleDefinition saved;
|
|
try
|
|
{
|
|
saved = _moduleStore.Save(module);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
MessageBox.Show(ex.Message, "Shigure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
LoadModules();
|
|
var index = _modules.FindIndex(existing => string.Equals(existing.Id, saved.Id, StringComparison.OrdinalIgnoreCase));
|
|
if (index >= 0)
|
|
{
|
|
_moduleList.SelectedIndex = index;
|
|
}
|
|
|
|
_runtimeRestartRequested();
|
|
}
|
|
|
|
private void DeleteSelectedModule()
|
|
{
|
|
if (_selectedModule is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var result = MessageBox.Show(
|
|
$"删除模块“{_selectedModule.Name}”?",
|
|
"Shigure",
|
|
MessageBoxButtons.YesNo,
|
|
MessageBoxIcon.Warning);
|
|
if (result != DialogResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_moduleStore.Delete(_selectedModule);
|
|
LoadModules();
|
|
_runtimeRestartRequested();
|
|
}
|
|
|
|
private bool TryReadModule(out ModuleDefinition module)
|
|
{
|
|
module = _selectedModule!.Clone();
|
|
module.Name = string.IsNullOrWhiteSpace(_nameBox.Text) ? "新模块" : _nameBox.Text.Trim();
|
|
module.Enabled = _enabledBox.Checked;
|
|
module.Match = new ModuleMatch
|
|
{
|
|
ClassId = ReadMatchCombo(_classBox),
|
|
SpecId = ReadMatchCombo(_specBox),
|
|
PartyType = ReadPartyTypeCombo(),
|
|
HeroTalent = ReadMatchCombo(_heroTalentBox)
|
|
};
|
|
|
|
module.Units = _units.Select(unit => unit.Clone()).ToList();
|
|
module.Counts = _counts.Select(count => count.Clone()).ToList();
|
|
module.Rules = ReadRules();
|
|
return true;
|
|
}
|
|
|
|
private List<ModuleRule> ReadRules()
|
|
{
|
|
var unitNames = new HashSet<string>(
|
|
_units.Where(unit => !string.IsNullOrWhiteSpace(unit.Name)).Select(unit => unit.Name),
|
|
StringComparer.Ordinal);
|
|
var rules = new List<ModuleRule>();
|
|
foreach (DataGridViewRow row in _rulesGrid.Rows)
|
|
{
|
|
if (row.IsNewRow)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var condition = CellText(row, "Condition");
|
|
var spell = CellText(row, "Spell");
|
|
var unitText = CellText(row, "Unit");
|
|
if (string.IsNullOrWhiteSpace(condition)
|
|
&& string.IsNullOrWhiteSpace(spell)
|
|
&& string.IsNullOrWhiteSpace(unitText))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 目标文本命中已定义动态单位名 → UnitName; 否则按数字 → Unit; 都不是则留空。
|
|
var isDynamic = unitNames.Contains(unitText);
|
|
rules.Add(new ModuleRule
|
|
{
|
|
Enabled = CellBool(row, "Enabled", defaultValue: true),
|
|
Condition = condition,
|
|
Unit = isDynamic ? null : ParseNullableInt(unitText),
|
|
UnitName = isDynamic ? unitText : null,
|
|
Spell = spell,
|
|
Hotkey = string.Empty,
|
|
Step = string.Empty
|
|
});
|
|
}
|
|
|
|
return rules;
|
|
}
|
|
|
|
private static void AddMatchField(TableLayoutPanel row, string label, TextBox box, int column)
|
|
{
|
|
row.Controls.Add(CreateLabel(label), column, 0);
|
|
StyleTextBox(box);
|
|
box.Dock = DockStyle.Fill;
|
|
row.Controls.Add(box, column + 1, 0);
|
|
}
|
|
|
|
private static void AddMatchField(TableLayoutPanel row, string label, ComboBox box, int column)
|
|
{
|
|
row.Controls.Add(CreateLabel(label), column, 0);
|
|
UiTheme.StyleComboBox(box);
|
|
box.Dock = DockStyle.Fill;
|
|
row.Controls.Add(box, column + 1, 0);
|
|
}
|
|
|
|
private static Label CreateLabel(string text)
|
|
{
|
|
return new Label
|
|
{
|
|
Text = text,
|
|
Dock = DockStyle.Fill,
|
|
ForeColor = UiTheme.Muted,
|
|
TextAlign = ContentAlignment.MiddleLeft,
|
|
AutoEllipsis = true
|
|
};
|
|
}
|
|
|
|
private static int MeasureLabelColumnWidth(string text, Font font)
|
|
{
|
|
return TextRenderer.MeasureText(text, font).Width + 18;
|
|
}
|
|
|
|
private static Button CreateUnitActionButton(string text, Color backColor, Color foreColor, bool bottomGap)
|
|
{
|
|
var button = UiTheme.CreateButton(text, backColor, foreColor);
|
|
button.AutoSize = false;
|
|
button.AutoEllipsis = true;
|
|
button.Height = 32;
|
|
button.Margin = new Padding(0, 0, 0, bottomGap ? 6 : 0);
|
|
button.Padding = new Padding(0);
|
|
button.TextAlign = ContentAlignment.MiddleCenter;
|
|
return button;
|
|
}
|
|
|
|
private static void LayoutUnitActionButtons(FlowLayoutPanel panel)
|
|
{
|
|
var width = Math.Max(0, panel.ClientSize.Width);
|
|
foreach (Control control in panel.Controls)
|
|
{
|
|
if (control is Button button)
|
|
{
|
|
button.Width = width;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void StyleTextBox(TextBox textBox)
|
|
{
|
|
textBox.BackColor = UiTheme.Field;
|
|
textBox.ForeColor = UiTheme.Text;
|
|
textBox.BorderStyle = BorderStyle.FixedSingle;
|
|
}
|
|
|
|
private static string FormatModuleListItem(ModuleDefinition module)
|
|
{
|
|
var enabled = module.Enabled ? "●" : "○";
|
|
var match = $"{FormatMatchValue(module.Match.ClassId)}/{FormatMatchValue(module.Match.SpecId)}/{FormatPartyTypeValue(module.Match.PartyType)}/{FormatMatchValue(module.Match.HeroTalent)}";
|
|
return $"{enabled} {module.Name} [{match}]";
|
|
}
|
|
|
|
private static string FormatMatchValue(int? value)
|
|
{
|
|
return value?.ToString() ?? "*";
|
|
}
|
|
|
|
private void SelectClass(int? value)
|
|
{
|
|
var index = FindMatchOption(_classBox, value);
|
|
if (index < 0 && value is not null)
|
|
{
|
|
_classBox.Items.Add(new MatchOption($"职业{value} ({value})", value));
|
|
index = _classBox.Items.Count - 1;
|
|
}
|
|
|
|
_classBox.SelectedIndex = index >= 0 ? index : 0;
|
|
ResetSpecOptions(_specBox, ReadMatchCombo(_classBox));
|
|
}
|
|
|
|
private void SelectSpec(int? value)
|
|
{
|
|
var index = FindMatchOption(_specBox, value);
|
|
if (index < 0 && value is not null)
|
|
{
|
|
_specBox.Items.Add(new MatchOption($"专精{value} ({value})", value));
|
|
index = _specBox.Items.Count - 1;
|
|
}
|
|
|
|
_specBox.SelectedIndex = index >= 0 ? index : 0;
|
|
ResetHeroTalentOptions(_heroTalentBox, ReadMatchCombo(_classBox), ReadMatchCombo(_specBox));
|
|
}
|
|
|
|
private void SelectHeroTalent(int? value)
|
|
{
|
|
var index = FindMatchOption(_heroTalentBox, value);
|
|
if (index < 0 && value is not null)
|
|
{
|
|
_heroTalentBox.Items.Add(new MatchOption($"英雄天赋{value} ({value})", value));
|
|
index = _heroTalentBox.Items.Count - 1;
|
|
}
|
|
|
|
_heroTalentBox.SelectedIndex = index >= 0 ? index : 0;
|
|
}
|
|
|
|
private static int? ReadMatchCombo(ComboBox comboBox)
|
|
{
|
|
return comboBox.SelectedItem is MatchOption option ? option.Value : null;
|
|
}
|
|
|
|
private static void ResetClassOptions(ComboBox comboBox)
|
|
{
|
|
comboBox.Items.Clear();
|
|
comboBox.Items.AddRange(ClassOptions);
|
|
comboBox.SelectedIndex = 0;
|
|
}
|
|
|
|
private static void ResetSpecOptions(ComboBox comboBox, int? classId)
|
|
{
|
|
comboBox.Items.Clear();
|
|
comboBox.Items.Add(new MatchOption("任意 (*)", null));
|
|
if (classId is not null)
|
|
{
|
|
foreach (var spec in ClassNames.GetSpecs(classId.Value))
|
|
{
|
|
comboBox.Items.Add(new MatchOption($"{spec.Name} ({spec.Id})", spec.Id));
|
|
}
|
|
}
|
|
|
|
comboBox.SelectedIndex = 0;
|
|
}
|
|
|
|
private static void ResetHeroTalentOptions(ComboBox comboBox, int? classId, int? specId)
|
|
{
|
|
comboBox.Items.Clear();
|
|
comboBox.Items.Add(new MatchOption("任意 (*)", null));
|
|
if (classId is not null && specId is not null)
|
|
{
|
|
foreach (var heroTalent in ClassNames.GetHeroTalents(classId.Value, specId.Value))
|
|
{
|
|
comboBox.Items.Add(new MatchOption($"{heroTalent.Name} ({heroTalent.Id})", heroTalent.Id));
|
|
}
|
|
}
|
|
|
|
comboBox.SelectedIndex = 0;
|
|
}
|
|
|
|
private static int FindMatchOption(ComboBox comboBox, int? value)
|
|
{
|
|
for (var i = 0; i < comboBox.Items.Count; i++)
|
|
{
|
|
if (comboBox.Items[i] is MatchOption option && option.Value == value)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static MatchOption[] BuildClassOptions()
|
|
{
|
|
return ClassNames.GetClasses()
|
|
.Select(item => new MatchOption($"{item.Name} ({item.Id})", item.Id))
|
|
.Prepend(new MatchOption("任意 (*)", null))
|
|
.ToArray();
|
|
}
|
|
|
|
private void SelectPartyType(string? value)
|
|
{
|
|
ResetPartyTypeOptions(_partyTypeBox);
|
|
var normalized = ModuleMatch.NormalizePartyTypeValue(value);
|
|
var index = FindPartyTypeOption(normalized);
|
|
if (index < 0 && !string.IsNullOrWhiteSpace(normalized))
|
|
{
|
|
_partyTypeBox.Items.Add(new PartyTypeOption($"自定义 ({normalized})", normalized));
|
|
index = _partyTypeBox.Items.Count - 1;
|
|
}
|
|
|
|
_partyTypeBox.SelectedIndex = index >= 0 ? index : 0;
|
|
}
|
|
|
|
private string? ReadPartyTypeCombo()
|
|
{
|
|
return _partyTypeBox.SelectedItem is PartyTypeOption option ? option.Value : null;
|
|
}
|
|
|
|
private static void ResetPartyTypeOptions(ComboBox comboBox)
|
|
{
|
|
comboBox.Items.Clear();
|
|
comboBox.Items.AddRange(PartyTypeOptions);
|
|
comboBox.SelectedIndex = 0;
|
|
}
|
|
|
|
private static int FindPartyTypeOption(string? value)
|
|
{
|
|
for (var i = 0; i < PartyTypeOptions.Length; i++)
|
|
{
|
|
if (string.Equals(PartyTypeOptions[i].Value, value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static string FormatPartyTypeValue(string? value)
|
|
{
|
|
return ModuleMatch.NormalizePartyTypeValue(value) switch
|
|
{
|
|
null => "*",
|
|
"0" => "单人",
|
|
"1-40" => "团队",
|
|
"46" => "队伍",
|
|
var other => other
|
|
};
|
|
}
|
|
|
|
private static string CellText(DataGridViewRow row, string columnName)
|
|
{
|
|
return row.Cells[columnName].Value?.ToString()?.Trim() ?? string.Empty;
|
|
}
|
|
|
|
private static bool CellBool(DataGridViewRow row, string columnName, bool defaultValue)
|
|
{
|
|
var value = row.Cells[columnName].Value;
|
|
return value switch
|
|
{
|
|
bool b => b,
|
|
string s when bool.TryParse(s, out var parsed) => parsed,
|
|
null => defaultValue,
|
|
_ => defaultValue
|
|
};
|
|
}
|
|
|
|
private static int? ParseNullableInt(string text)
|
|
{
|
|
return int.TryParse(text, out var value) ? value : null;
|
|
}
|
|
|
|
private sealed record PartyTypeOption(string Text, string? Value)
|
|
{
|
|
public override string ToString()
|
|
{
|
|
return Text;
|
|
}
|
|
}
|
|
|
|
private sealed record MatchOption(string Text, int? Value)
|
|
{
|
|
public override string ToString()
|
|
{
|
|
return Text;
|
|
}
|
|
}
|
|
}
|