From aaba54098b1bd9e969ffb01e6413303e863eb81c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Korczy=C5=84ski?= Date: Sun, 21 Aug 2022 13:21:35 +0200 Subject: [PATCH] [Event AI] Event AI editor --- LoaderAvalonia/LoaderAvalonia.csproj | 1 + LoaderAvalonia/Program.cs | 4 +- WDE.AzerothCore/AzerothCoreVersion.cs | 3 +- WDE.CMaNGOS/CMaNGOSVersion.cs | 3 +- WDE.DatabaseEditors/DatabaseEditorsModule.cs | 3 + .../cmangos/creature_ai_summons.json | 61 + .../cmangos/creature_spell_list.json | 91 ++ .../cmangos/creature_spell_list_entry.json | 46 + .../cmangos/creature_spell_targeting.json | 56 + .../cmangos/dbscript_random_template.json | 57 + .../CreatureTemplateSpellListIdParameter.cs | 40 + ...criptRandomTemplateTargetValueParameter.cs | 61 + .../Editor/FastGroupingWrapPanel.cs | 256 ++++ WDE.EventAiEditor.Avalonia/Editor/README.md | 1 + .../Editor/UserControls/EventAiActionView.cs | 74 ++ .../UserControls/EventAiEventFlagsView.cs | 172 +++ .../Editor/UserControls/EventAiEventView.cs | 73 ++ .../Editor/UserControls/EventAiPanelLayout.cs | 646 ++++++++++ .../Editor/UserControls/EventAiView.axaml | 111 ++ .../Editor/UserControls/EventAiView.axaml.cs | 20 + .../Editor/UserControls/MiniEventIcon.cs | 25 + .../SelectableTemplatedControl.cs | 118 ++ .../Views/Editing/ParameterEditorView.cs | 48 + .../Views/Editing/ParametersEditView.axaml | 53 + .../Views/Editing/ParametersEditView.axaml.cs | 20 + .../Editor/Views/EventAiEditorView.axaml | 15 + .../Editor/Views/EventAiEditorView.axaml.cs | 20 + .../Editor/Views/EventAiSelectView.axaml | 149 +++ .../Editor/Views/EventAiSelectView.axaml.cs | 47 + .../Views/ParameterDataTemplateSelector.cs | 39 + .../Editor/Views/ToolEventAiEditorView.axaml | 47 + .../Views/ToolEventAiEditorView.axaml.cs | 18 + .../EventAiAvaloniaModule.cs | 10 + .../Themes/ColorsDark.axaml | 42 + .../Themes/ColorsLight.axaml | 39 + .../Themes/Generic.axaml | 220 ++++ .../Themes/IndentToMarginConverter.cs | 27 + .../WDE.EventAiEditor.Avalonia.csproj | 25 + WDE.EventAiEditor/Data/DataJsonStructs.cs | 152 +++ .../Data/EventActionListProvider.cs | 59 + WDE.EventAiEditor/Data/EventAiDataManager.cs | 193 +++ WDE.EventAiEditor/Data/EventAiDataProvider.cs | 57 + .../Data/EventAiDataSerializationProvider.cs | 27 + WDE.EventAiEditor/Data/EventAiFactory.cs | 169 +++ .../Data/EventAiRawDataProvider.cs | 35 + .../Data/IEventAiDataProvider.cs | 35 + .../Data/IEventAiDataSerializationProvider.cs | 10 + WDE.EventAiEditor/Data/IEventAiFactory.cs | 31 + .../Editor/DropActionsConditionsArgs.cs | 10 + WDE.EventAiEditor/Editor/EventAiSerializer.cs | 106 ++ WDE.EventAiEditor/Editor/IEditorFeatures.cs | 7 + .../Editor/IEventAiDatabaseProvider.cs | 20 + WDE.EventAiEditor/Editor/IEventAiExporter.cs | 12 + WDE.EventAiEditor/Editor/IEventAiImporter.cs | 12 + .../ViewModels/Editing/EditableActionData.cs | 22 + .../EditableParameterActionViewModel.cs | 34 + .../Editing/EditableParameterViewModel.cs | 173 +++ .../Editing/IEditableParameterViewModel.cs | 12 + .../Editing/ParametersEditViewModel.cs | 143 +++ .../ViewModels/EventAiEditorViewModel.cs | 999 +++++++++++++++ .../ViewModels/EventAiSelectViewModel.cs | 244 ++++ .../Editor/ViewModels/EventAiTeachingTips.cs | 137 +++ .../Editor/ViewModels/EventOrActionItem.cs | 70 ++ .../ViewModels/IToolEventAiEditorViewModel.cs | 12 + .../Editor/ViewModels/NewActionViewModel.cs | 31 + .../ViewModels/ToolEventAiEditorViewModel.cs | 46 + WDE.EventAiEditor/EventAiIconBaseProvider.cs | 12 + WDE.EventAiEditor/EventAiSolutionItem.cs | 51 + .../History/EventAiHistoryHandler.cs | 375 ++++++ .../Inspections/DuplicateEventsInspection.cs | 39 + .../Inspections/IActionInspection.cs | 14 + .../Inspections/IEventActionInspection.cs | 20 + .../Inspections/IEventInspection.cs | 14 + .../Inspections/IScriptInspection.cs | 15 + .../Inspections/IndentationInspection.cs | 45 + .../Inspections/InspectionResult.cs | 11 + .../Inspections/InspectorService.cs | 205 ++++ .../Inspections/JsonConditionInspection.cs | 50 + .../Inspections/MinMaxInspection.cs | 36 + .../Inspections/NonZeroInspection.cs | 32 + .../Inspections/ParameterRangeInspection.cs | 69 ++ .../Inspections/PercentValueInspection.cs | 33 + .../Inspections/TooManyActionsInspection.cs | 108 ++ .../Inspections/UnusedParameterInspection.cs | 59 + WDE.EventAiEditor/Models/DescriptionRule.cs | 27 + WDE.EventAiEditor/Models/EventAiAction.cs | 160 +++ WDE.EventAiEditor/Models/EventAiBase.cs | 182 +++ .../Models/EventAiBaseElement.cs | 121 ++ WDE.EventAiEditor/Models/EventAiConstants.cs | 9 + WDE.EventAiEditor/Models/EventAiEvent.cs | 332 +++++ .../Models/EventAiReloadRemoteCommand.cs | 19 + WDE.EventAiEditor/Models/EventAiScript.cs | 19 + .../Models/EventAiValidationContext.cs | 26 + .../Models/Helpers/EventAiSelectionHelper.cs | 112 ++ .../Models/IEventAiSolutionItem.cs | 11 + .../Parameters/ActionSummonIdParameter.cs | 55 + .../Parameters/EventAiNewTextParameter.cs | 81 ++ .../EventAiNewTextRandomTemplateParameter.cs | 118 ++ .../EventAiSetFieldValueParameter.cs | 72 ++ .../Parameters/MangosEmoteParameter.cs | 40 + .../Parameters/StartRelayIdParameter.cs | 59 + .../Providers/EventAiNameProviderBase.cs | 49 + .../Providers/EventAiRelatedProviderBase.cs | 16 + .../Services/EventAiBaseFindAnywhereSource.cs | 79 ++ .../Services/FavouriteEventAiService.cs | 44 + .../Services/IFavouriteEventAiService.cs | 10 + WDE.EventAiEditor/Utils/Extensions.cs | 9 + WDE.EventAiEditor/Utils/FlagJsonConverter.cs | 36 + .../Validation/Antlr/EventAiValidation.g4 | 61 + .../Validation/Antlr/EventAiValidator.cs | 81 ++ .../Antlr/Visitors/BinaryOperatorVisitor.cs | 47 + .../Antlr/Visitors/BoolExpressionVisitor.cs | 89 ++ .../Antlr/Visitors/IntExpressionVisitor.cs | 82 ++ .../Validation/IEventAiValidationContext.cs | 14 + .../Validation/IEventAiValidator.cs | 7 + .../Validation/ValidationParseException.cs | 12 + WDE.EventAiEditor/WDE.EventAiEditor.csproj | 54 + .../Data/EventAiDataJsonProvider.cs | 29 + .../Editor/MangosEditorFeatures.cs | 12 + .../Editor/MangosEventAiDatabaseProvider.cs | 32 + .../EventAiCreatureProvider.cs | 48 + .../EventAiData/actions.json | 1080 +++++++++++++++++ .../EventAiData/actions_groups.json | 128 ++ .../EventAiData/events.json | 944 ++++++++++++++ .../EventAiData/events_groups.json | 62 + WDE.MangosEventAiEditor/EventAiModule.cs | 41 + .../EventAiSolutionItemProvider.cs | 44 + .../Exporter/ExporterHelper.cs | 116 ++ .../Exporter/MangosEventAiExporter.cs | 100 ++ .../Exporter/MangosEventAiImporter.cs | 56 + .../Providers/EventAiDeserializer.cs | 32 + .../Providers/EventAiEditorProvider.cs | 27 + .../Providers/EventAiIconProvider.cs | 11 + .../Providers/EventAiNameProvider.cs | 21 + .../Providers/EventAiQueryParser.cs | 48 + .../Providers/EventAiRelatedProvider.cs | 11 + .../Providers/EventAiSqlGenerator.cs | 55 + .../TrinityEventAiFindAnywhereSource.cs | 23 + .../WDE.MangosEventAiEditor.csproj | 35 + .../Database/World/CachedDatabaseProvider.cs | 2 + .../Database/World/WorldDatabaseDecorator.cs | 2 + .../Parameters/mangos_parameters.json | 85 ++ WDE.Trinity/TrinityCataclysmVersion.cs | 3 +- WDE.Trinity/TrinityMasterVersion.cs | 3 +- WDE.Trinity/TrinityWrathVersion.cs | 3 +- .../Database/BaseDatabaseProvider.cs | 20 +- .../Database/DatabaseProviderWoTLK.cs | 2 +- .../WDE.CMMySqlDatabase/DatabaseResolver.cs | 9 +- .../Models/CreatureAiSummon.cs | 31 + .../Models/Databases/BaseDatabaseTables.cs | 9 +- .../Models/DbScriptRandomTemplate.cs | 29 + .../WDE.CMMySqlDatabase/Models/EventAiLine.cs | 38 + .../MySqlDatabaseModule.cs | 2 +- .../WorldDatabaseProvider.cs | 17 +- .../WDE.Common/CoreVersion/ICoreVersion.cs | 9 + .../WDE.Common/DBC/IDbcStore.cs | 2 + .../WDE.Common/Database/ICreatureAiSummon.cs | 12 + .../WDE.Common/Database/IDatabaseProvider.cs | 19 + .../Database/IDbScriptRandomTemplate.cs | 10 + .../WDE.Common/Database/IEventAiLine.cs | 183 +++ .../WDE.DbcStore/DbcStore.cs | 8 +- WoWDatabaseEditor.sln | 21 + .../CoreVersion/UnspecifiedCoreVersion.cs | 3 +- WoWDatabaseEditor/Icons/document_dice.png | Bin 0 -> 708 bytes WoWDatabaseEditor/Icons/document_dice@2x.png | Bin 0 -> 863 bytes WoWDatabaseEditor/Icons/document_dice_big.png | Bin 0 -> 863 bytes .../Icons/document_dice_big@2x.png | Bin 0 -> 1278 bytes WoWDatabaseEditorCore.Avalonia/App.xaml | 1 + 168 files changed, 12376 insertions(+), 22 deletions(-) create mode 100644 WDE.DatabaseEditors/DbDefinitions/cmangos/creature_ai_summons.json create mode 100644 WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list.json create mode 100644 WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list_entry.json create mode 100644 WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_targeting.json create mode 100644 WDE.DatabaseEditors/DbDefinitions/cmangos/dbscript_random_template.json create mode 100644 WDE.DatabaseEditors/Parameters/CreatureTemplateSpellListIdParameter.cs create mode 100644 WDE.DatabaseEditors/Parameters/DbScriptRandomTemplateTargetValueParameter.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/FastGroupingWrapPanel.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/README.md create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiActionView.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventFlagsView.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventView.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiPanelLayout.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/MiniEventIcon.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/UserControls/SelectableTemplatedControl.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParameterEditorView.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParametersEditView.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParametersEditView.axaml.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/EventAiEditorView.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/EventAiEditorView.axaml.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/EventAiSelectView.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/EventAiSelectView.axaml.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/ParameterDataTemplateSelector.cs create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/ToolEventAiEditorView.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Editor/Views/ToolEventAiEditorView.axaml.cs create mode 100644 WDE.EventAiEditor.Avalonia/EventAiAvaloniaModule.cs create mode 100644 WDE.EventAiEditor.Avalonia/Themes/ColorsDark.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Themes/ColorsLight.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Themes/Generic.axaml create mode 100644 WDE.EventAiEditor.Avalonia/Themes/IndentToMarginConverter.cs create mode 100644 WDE.EventAiEditor.Avalonia/WDE.EventAiEditor.Avalonia.csproj create mode 100644 WDE.EventAiEditor/Data/DataJsonStructs.cs create mode 100644 WDE.EventAiEditor/Data/EventActionListProvider.cs create mode 100644 WDE.EventAiEditor/Data/EventAiDataManager.cs create mode 100644 WDE.EventAiEditor/Data/EventAiDataProvider.cs create mode 100644 WDE.EventAiEditor/Data/EventAiDataSerializationProvider.cs create mode 100644 WDE.EventAiEditor/Data/EventAiFactory.cs create mode 100644 WDE.EventAiEditor/Data/EventAiRawDataProvider.cs create mode 100644 WDE.EventAiEditor/Data/IEventAiDataProvider.cs create mode 100644 WDE.EventAiEditor/Data/IEventAiDataSerializationProvider.cs create mode 100644 WDE.EventAiEditor/Data/IEventAiFactory.cs create mode 100644 WDE.EventAiEditor/Editor/DropActionsConditionsArgs.cs create mode 100644 WDE.EventAiEditor/Editor/EventAiSerializer.cs create mode 100644 WDE.EventAiEditor/Editor/IEditorFeatures.cs create mode 100644 WDE.EventAiEditor/Editor/IEventAiDatabaseProvider.cs create mode 100644 WDE.EventAiEditor/Editor/IEventAiExporter.cs create mode 100644 WDE.EventAiEditor/Editor/IEventAiImporter.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/Editing/EditableActionData.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/Editing/EditableParameterActionViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/Editing/EditableParameterViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/Editing/IEditableParameterViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/Editing/ParametersEditViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/EventAiEditorViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/EventAiSelectViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/EventAiTeachingTips.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/EventOrActionItem.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/IToolEventAiEditorViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/NewActionViewModel.cs create mode 100644 WDE.EventAiEditor/Editor/ViewModels/ToolEventAiEditorViewModel.cs create mode 100644 WDE.EventAiEditor/EventAiIconBaseProvider.cs create mode 100644 WDE.EventAiEditor/EventAiSolutionItem.cs create mode 100644 WDE.EventAiEditor/History/EventAiHistoryHandler.cs create mode 100644 WDE.EventAiEditor/Inspections/DuplicateEventsInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/IActionInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/IEventActionInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/IEventInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/IScriptInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/IndentationInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/InspectionResult.cs create mode 100644 WDE.EventAiEditor/Inspections/InspectorService.cs create mode 100644 WDE.EventAiEditor/Inspections/JsonConditionInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/MinMaxInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/NonZeroInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/ParameterRangeInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/PercentValueInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/TooManyActionsInspection.cs create mode 100644 WDE.EventAiEditor/Inspections/UnusedParameterInspection.cs create mode 100644 WDE.EventAiEditor/Models/DescriptionRule.cs create mode 100644 WDE.EventAiEditor/Models/EventAiAction.cs create mode 100644 WDE.EventAiEditor/Models/EventAiBase.cs create mode 100644 WDE.EventAiEditor/Models/EventAiBaseElement.cs create mode 100644 WDE.EventAiEditor/Models/EventAiConstants.cs create mode 100644 WDE.EventAiEditor/Models/EventAiEvent.cs create mode 100644 WDE.EventAiEditor/Models/EventAiReloadRemoteCommand.cs create mode 100644 WDE.EventAiEditor/Models/EventAiScript.cs create mode 100644 WDE.EventAiEditor/Models/EventAiValidationContext.cs create mode 100644 WDE.EventAiEditor/Models/Helpers/EventAiSelectionHelper.cs create mode 100644 WDE.EventAiEditor/Models/IEventAiSolutionItem.cs create mode 100644 WDE.EventAiEditor/Parameters/ActionSummonIdParameter.cs create mode 100644 WDE.EventAiEditor/Parameters/EventAiNewTextParameter.cs create mode 100644 WDE.EventAiEditor/Parameters/EventAiNewTextRandomTemplateParameter.cs create mode 100644 WDE.EventAiEditor/Parameters/EventAiSetFieldValueParameter.cs create mode 100644 WDE.EventAiEditor/Parameters/MangosEmoteParameter.cs create mode 100644 WDE.EventAiEditor/Parameters/StartRelayIdParameter.cs create mode 100644 WDE.EventAiEditor/Providers/EventAiNameProviderBase.cs create mode 100644 WDE.EventAiEditor/Providers/EventAiRelatedProviderBase.cs create mode 100644 WDE.EventAiEditor/Services/EventAiBaseFindAnywhereSource.cs create mode 100644 WDE.EventAiEditor/Services/FavouriteEventAiService.cs create mode 100644 WDE.EventAiEditor/Services/IFavouriteEventAiService.cs create mode 100644 WDE.EventAiEditor/Utils/Extensions.cs create mode 100644 WDE.EventAiEditor/Utils/FlagJsonConverter.cs create mode 100644 WDE.EventAiEditor/Validation/Antlr/EventAiValidation.g4 create mode 100644 WDE.EventAiEditor/Validation/Antlr/EventAiValidator.cs create mode 100644 WDE.EventAiEditor/Validation/Antlr/Visitors/BinaryOperatorVisitor.cs create mode 100644 WDE.EventAiEditor/Validation/Antlr/Visitors/BoolExpressionVisitor.cs create mode 100644 WDE.EventAiEditor/Validation/Antlr/Visitors/IntExpressionVisitor.cs create mode 100644 WDE.EventAiEditor/Validation/IEventAiValidationContext.cs create mode 100644 WDE.EventAiEditor/Validation/IEventAiValidator.cs create mode 100644 WDE.EventAiEditor/Validation/ValidationParseException.cs create mode 100644 WDE.EventAiEditor/WDE.EventAiEditor.csproj create mode 100644 WDE.MangosEventAiEditor/Data/EventAiDataJsonProvider.cs create mode 100644 WDE.MangosEventAiEditor/Editor/MangosEditorFeatures.cs create mode 100644 WDE.MangosEventAiEditor/Editor/MangosEventAiDatabaseProvider.cs create mode 100644 WDE.MangosEventAiEditor/EventAiCreatureProvider.cs create mode 100644 WDE.MangosEventAiEditor/EventAiData/actions.json create mode 100644 WDE.MangosEventAiEditor/EventAiData/actions_groups.json create mode 100644 WDE.MangosEventAiEditor/EventAiData/events.json create mode 100644 WDE.MangosEventAiEditor/EventAiData/events_groups.json create mode 100644 WDE.MangosEventAiEditor/EventAiModule.cs create mode 100644 WDE.MangosEventAiEditor/EventAiSolutionItemProvider.cs create mode 100644 WDE.MangosEventAiEditor/Exporter/ExporterHelper.cs create mode 100644 WDE.MangosEventAiEditor/Exporter/MangosEventAiExporter.cs create mode 100644 WDE.MangosEventAiEditor/Exporter/MangosEventAiImporter.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiDeserializer.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiEditorProvider.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiIconProvider.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiNameProvider.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiQueryParser.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiRelatedProvider.cs create mode 100644 WDE.MangosEventAiEditor/Providers/EventAiSqlGenerator.cs create mode 100644 WDE.MangosEventAiEditor/Services/TrinityEventAiFindAnywhereSource.cs create mode 100644 WDE.MangosEventAiEditor/WDE.MangosEventAiEditor.csproj create mode 100644 WDE.Parameters/Parameters/mangos_parameters.json create mode 100644 WoWDatabaseEditor.Common/WDE.CMMySqlDatabase/Models/CreatureAiSummon.cs create mode 100644 WoWDatabaseEditor.Common/WDE.CMMySqlDatabase/Models/DbScriptRandomTemplate.cs create mode 100644 WoWDatabaseEditor.Common/WDE.CMMySqlDatabase/Models/EventAiLine.cs create mode 100644 WoWDatabaseEditor.Common/WDE.Common/Database/ICreatureAiSummon.cs create mode 100644 WoWDatabaseEditor.Common/WDE.Common/Database/IDbScriptRandomTemplate.cs create mode 100644 WoWDatabaseEditor.Common/WDE.Common/Database/IEventAiLine.cs create mode 100644 WoWDatabaseEditor/Icons/document_dice.png create mode 100644 WoWDatabaseEditor/Icons/document_dice@2x.png create mode 100644 WoWDatabaseEditor/Icons/document_dice_big.png create mode 100644 WoWDatabaseEditor/Icons/document_dice_big@2x.png diff --git a/LoaderAvalonia/LoaderAvalonia.csproj b/LoaderAvalonia/LoaderAvalonia.csproj index e5af3a223..3de43fd19 100644 --- a/LoaderAvalonia/LoaderAvalonia.csproj +++ b/LoaderAvalonia/LoaderAvalonia.csproj @@ -30,6 +30,7 @@ + diff --git a/LoaderAvalonia/Program.cs b/LoaderAvalonia/Program.cs index 5abe37de3..00f7c87d5 100644 --- a/LoaderAvalonia/Program.cs +++ b/LoaderAvalonia/Program.cs @@ -30,6 +30,7 @@ using WDE.WoWHeadConnector; using WDE.AnniversaryInfo; using WDE.EventScriptsEditor; +using WDE.MangosEventAiEditor; using WDE.MapSpawns; using WDE.PathPreviewTool; @@ -74,7 +75,8 @@ public static void Main(string[] args) typeof(AnniversaryModule), typeof(EventScriptsModule), typeof(MapSpawnsModule), - typeof(PathPreviewToolModule) + typeof(PathPreviewToolModule), + typeof(EventAiModule) }; WoWDatabaseEditorCore.Avalonia.Program.PreloadedModules = modules; WoWDatabaseEditorCore.Avalonia.Program.Main(args); diff --git a/WDE.AzerothCore/AzerothCoreVersion.cs b/WDE.AzerothCore/AzerothCoreVersion.cs index be35cfa8a..78b28e647 100644 --- a/WDE.AzerothCore/AzerothCoreVersion.cs +++ b/WDE.AzerothCore/AzerothCoreVersion.cs @@ -8,13 +8,14 @@ namespace WDE.AzerothCore { [AutoRegister] [SingleInstance] - public class AzerothCoreVersion : ICoreVersion, IDatabaseFeatures, ISmartScriptFeatures, IConditionFeatures, IGameVersionFeatures + public class AzerothCoreVersion : ICoreVersion, IDatabaseFeatures, ISmartScriptFeatures, IConditionFeatures, IGameVersionFeatures, IEventAiFeatures { public string Tag => "Azeroth"; public string FriendlyName => "AzerothCore Wrath of the Lich King"; public ISmartScriptFeatures SmartScriptFeatures => this; public IConditionFeatures ConditionFeatures => this; public IGameVersionFeatures GameVersionFeatures => this; + public IEventAiFeatures EventAiFeatures => this; public IDatabaseFeatures DatabaseFeatures => this; public bool SupportsRbac => false; public bool SupportsConditionTargetVictim => true; diff --git a/WDE.CMaNGOS/CMaNGOSVersion.cs b/WDE.CMaNGOS/CMaNGOSVersion.cs index bc25ea561..b06ef6f55 100644 --- a/WDE.CMaNGOS/CMaNGOSVersion.cs +++ b/WDE.CMaNGOS/CMaNGOSVersion.cs @@ -8,13 +8,14 @@ namespace WDE.CMaNGOS { [AutoRegister] [SingleInstance] - public class CMaNGOSCoreVersion : ICoreVersion, IDatabaseFeatures, ISmartScriptFeatures, IConditionFeatures, IGameVersionFeatures + public class CMaNGOSCoreVersion : ICoreVersion, IDatabaseFeatures, ISmartScriptFeatures, IConditionFeatures, IGameVersionFeatures, IEventAiFeatures { public string Tag => "CMaNGOS-WoTLK"; public string FriendlyName => "CMaNGOS Wrath of the Lich King"; public ISmartScriptFeatures SmartScriptFeatures => this; public IConditionFeatures ConditionFeatures => this; public IGameVersionFeatures GameVersionFeatures => this; + public IEventAiFeatures EventAiFeatures => this; public IDatabaseFeatures DatabaseFeatures => this; public bool SupportsRbac => false; public bool SupportsConditionTargetVictim => false; diff --git a/WDE.DatabaseEditors/DatabaseEditorsModule.cs b/WDE.DatabaseEditors/DatabaseEditorsModule.cs index 3986bc033..ce3e04e75 100644 --- a/WDE.DatabaseEditors/DatabaseEditorsModule.cs +++ b/WDE.DatabaseEditors/DatabaseEditorsModule.cs @@ -38,9 +38,12 @@ public override void OnInitialized(IContainerProvider containerProvider) this.containerProvider = containerProvider; containerProvider.Resolve().OnEvent().SubscribeOnce(_ => { + var parameterPickerService = containerProvider.Resolve(); containerProvider.Resolve(); var factory = containerProvider.Resolve(); factory.Register("BroadcastTextParameter", containerProvider.Resolve()); + factory.RegisterDepending("CreatureTemplateSpellListIdParameter", "CreatureParameter", (a) => new CreatureTemplateSpellListIdParameter(a, parameterPickerService)); + factory.RegisterDepending("DbScriptRandomTemplateTargetValueParameter", "BroadcastTextParameter", bcast => new DbScriptRandomTemplateTargetValueParameter(containerProvider.Resolve(), bcast)); factory.Register("LootReferenceParameter", containerProvider.Resolve()); factory.Register("EquipmentCreatureGuidParameter", containerProvider.Resolve()); factory.Register("CreatureGUIDParameter", factory.Factory("TableReference(creature#guid)Parameter")); diff --git a/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_ai_summons.json b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_ai_summons.json new file mode 100644 index 000000000..900cb30ac --- /dev/null +++ b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_ai_summons.json @@ -0,0 +1,61 @@ +{ + "id": "creature_ai_summons", + "compatibility": [ + "CMaNGOS-WoTLK" + ], + "name": "Creature Ai Summons", + "description": "Positions for summons in Event AI", + "table_name": "creature_ai_summons", + "table_index_name": "id", + "record_mode": "SingleRow", + "group_name": "Event AI", + "icon_path": "Icons/document_creature_summon_groups.png", + "reload_command": "reload creature_ai_summons", + "primary_key": [ + "id" + ], + "groups": [ + { + "group_name": "group", + "fields": [ + { + "name": "Id", + "db_column_name": "id", + "value_type": "uint" + }, + { + "name": "Position X", + "db_column_name": "position_x", + "value_type": "float" + }, + { + "name": "Position Y", + "db_column_name": "position_y", + "value_type": "float" + }, + { + "name": "Position Z", + "db_column_name": "position_z", + "value_type": "float" + }, + { + "name": "Orientation", + "db_column_name": "orientation", + "value_type": "float" + }, + { + "name": "Spawntimesecs", + "db_column_name": "spawntimesecs", + "value_type": "uint", + "default": 120 + }, + { + "name": "Comment", + "db_column_name": "comment", + "value_type": "string", + "default": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list.json b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list.json new file mode 100644 index 000000000..664afd70f --- /dev/null +++ b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list.json @@ -0,0 +1,91 @@ +{ + "id": "creature_spell_list", + "compatibility": [ + "CMaNGOS-WoTLK" + ], + "name": "Creature Spell List", + "description": "Used to define spells per creature.", + "table_name": "creature_spell_list", + "table_index_name": "Id", + "record_mode": "SingleRow", + "group_name": "Combat", + "icon_path": "Icons/document_creature_spell.png", + "picker": "Parameter", + "primary_key": [ + "Id", + "Position" + ], + "groups": [ + { + "group_name": "group", + "fields": [ + { + "name": "Id", + "db_column_name": "Id", + "value_type": "CreatureTemplateSpellListIdParameter" + }, + { + "name": "Position", + "db_column_name": "Position", + "value_type": "int" + }, + { + "name": "Spell", + "db_column_name": "SpellId", + "value_type": "SpellParameter" + }, + { + "name": "Initial Min", + "db_column_name": "InitialMin", + "value_type": "int" + }, + { + "name": "Initial Max", + "db_column_name": "InitialMax", + "value_type": "int" + }, + { + "name": "Repeat Min", + "db_column_name": "RepeatMin", + "value_type": "int" + }, + { + "name": "Repeat Max", + "db_column_name": "RepeatMax", + "value_type": "int" + }, + { + "name": "Flags", + "db_column_name": "Flags", + "value_type": "MangosSpellListFlagsParameter" + }, + { + "name": "Target", + "db_column_name": "TargetId", + "value_type": "TableReference(creature_spell_targeting#Id)Parameter" + }, + { + "name": "Relay Script Id", + "db_column_name": "ScriptId", + "value_type": "int", + "zero_is_blank": true + }, + { + "name": "Availability", + "db_column_name": "Availability", + "value_type": "PercentageParameter" + }, + { + "name": "Probability", + "db_column_name": "Probability", + "value_type": "int" + }, + { + "name": "Comment", + "db_column_name": "Comments", + "value_type": "string" + } + ] + } + ] +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list_entry.json b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list_entry.json new file mode 100644 index 000000000..2d7440deb --- /dev/null +++ b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_list_entry.json @@ -0,0 +1,46 @@ +{ + "id": "creature_spell_list_entry", + "compatibility": [ + "CMaNGOS-WoTLK" + ], + "name": "Creature Spell List Entry", + "description": "Holds basic chance of a ai to perform either a SPELL_LIST_FLAG_SUPPORT_ACTION or SPELL_LIST_FLAG_RANGED_ACTION.", + "table_name": "creature_spell_list_entry", + "table_index_name": "Id", + "record_mode": "SingleRow", + "group_name": "Combat", + "skip_quick_load": true, + "icon_path": "Icons/document_creature_spell_group.png", + "reload_command": "reload creature_spell_list_entry", + "primary_key": [ + "Id" + ], + "groups": [ + { + "group_name": "group", + "fields": [ + { + "name": "Id", + "db_column_name": "Id", + "value_type": "int", + "read_only": true + }, + { + "name": "Name", + "db_column_name": "Name", + "value_type": "string" + }, + { + "name": "Chance Support Action", + "db_column_name": "ChanceSupportAction", + "value_type": "int" + }, + { + "name": "Chance Ranged Attack", + "db_column_name": "ChanceRangedAttack", + "value_type": "int" + } + ] + } + ] +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_targeting.json b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_targeting.json new file mode 100644 index 000000000..b232dc36a --- /dev/null +++ b/WDE.DatabaseEditors/DbDefinitions/cmangos/creature_spell_targeting.json @@ -0,0 +1,56 @@ +{ + "id": "creature_spell_targeting", + "compatibility": [ + "CMaNGOS-WoTLK" + ], + "name": "Creature Spell Targeting", + "description": "Spell targeting data", + "table_name": "creature_spell_targeting", + "table_index_name": "Id", + "record_mode": "SingleRow", + "group_name": "Combat", + "skip_quick_load": true, + "icon_path": "Icons/document_creature_spell_group.png", + "reload_command": "reload creature_spell_targeting", + "primary_key": [ + "Id" + ], + "groups": [ + { + "group_name": "group", + "fields": [ + { + "name": "Id", + "db_column_name": "Id", + "value_type": "int", + "read_only": true + }, + { + "name": "Type", + "db_column_name": "Type", + "value_type": "CreatureSpellTargetingTypeParameter" + }, + { + "name": "Param 1", + "db_column_name": "Param1", + "value_type": "int" + }, + { + "name": "Param 2", + "db_column_name": "Param2", + "value_type": "int" + }, + { + "name": "Param 3", + "db_column_name": "Param3", + "value_type": "int" + }, + { + "name": "Comments", + "db_column_name": "Comments", + "value_type": "string" + } + ] + } + ] +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/DbDefinitions/cmangos/dbscript_random_template.json b/WDE.DatabaseEditors/DbDefinitions/cmangos/dbscript_random_template.json new file mode 100644 index 000000000..0628895e2 --- /dev/null +++ b/WDE.DatabaseEditors/DbDefinitions/cmangos/dbscript_random_template.json @@ -0,0 +1,57 @@ +{ + "id": "dbscript_random_templates", + "compatibility": [ + "CMaNGOS-WoTLK" + ], + "name": "Db script Random Templates", + "description": "Random templates used to define random texts or relay script IDs", + "table_name": "dbscript_random_templates", + "table_index_name": "id", + "record_mode": "SingleRow", + "group_name": "Database scripts", + "icon_path": "Icons/document_dice.png", + "primary_key": [ + "id", + "type", + "target_id" + ], + "groups": [ + { + "group_name": "group", + "fields": [ + { + "name": "Id", + "db_column_name": "id", + "value_type": "uint", + "preferred_width": 40.0 + }, + { + "name": "Type", + "db_column_name": "type", + "value_type": "MangosDbScriptRandomTemplateTypeParameter", + "preferred_width": 55.0 + }, + { + "name": "Target Value", + "db_column_name": "target_id", + "value_type": "DbScriptRandomTemplateTargetValueParameter", + "preferred_width": 200.0 + }, + { + "name": "Chance", + "db_column_name": "chance", + "value_type": "PercentageParameter", + "preferred_width": 40.0, + "zero_is_blank": true + }, + { + "name": "Comment", + "db_column_name": "comments", + "value_type": "string", + "default": "", + "can_be_null": true + } + ] + } + ] +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/Parameters/CreatureTemplateSpellListIdParameter.cs b/WDE.DatabaseEditors/Parameters/CreatureTemplateSpellListIdParameter.cs new file mode 100644 index 000000000..727651d90 --- /dev/null +++ b/WDE.DatabaseEditors/Parameters/CreatureTemplateSpellListIdParameter.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using WDE.Common.Parameters; + +namespace WDE.DatabaseEditors.Parameters; + +public class CreatureTemplateSpellListIdParameter : ICustomPickerParameter +{ + private readonly IParameter creatureTemplates; + private readonly IParameterPickerService parameterPickerService; + + public CreatureTemplateSpellListIdParameter(IParameter creatureTemplates, + IParameterPickerService parameterPickerService) + { + this.creatureTemplates = creatureTemplates; + this.parameterPickerService = parameterPickerService; + } + + public async Task<(long, bool)> PickValue(long value) + { + var phaseId = value % 100; + var result = await parameterPickerService.PickParameter(creatureTemplates, value / 100); + if (result.ok) + return (result.value * 100 + phaseId, true); + return (0, false); + } + + public string? Prefix => null; + public bool HasItems => true; + public bool AllowUnknownItems => true; + + public string ToString(long value) + { + var entry = value / 100; + var phaseId = value % 100; + return $"{creatureTemplates.ToString(entry)} - Phase {phaseId}"; + } + + public Dictionary? Items => null; +} \ No newline at end of file diff --git a/WDE.DatabaseEditors/Parameters/DbScriptRandomTemplateTargetValueParameter.cs b/WDE.DatabaseEditors/Parameters/DbScriptRandomTemplateTargetValueParameter.cs new file mode 100644 index 000000000..5147f6b63 --- /dev/null +++ b/WDE.DatabaseEditors/Parameters/DbScriptRandomTemplateTargetValueParameter.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using WDE.Common.Database; +using WDE.Common.Parameters; +using WDE.DatabaseEditors.Models; + +namespace WDE.DatabaseEditors.Parameters; + +public class DbScriptRandomTemplateTargetValueParameter : IAsyncContextualParameter, ICustomPickerContextualParameter +{ + private readonly IParameterPickerService pickerService; + private readonly IAsyncParameter? broadcastTextsParameter; + + public DbScriptRandomTemplateTargetValueParameter(IParameterPickerService pickerService, + IParameter broadcastTextsParameter) + { + this.pickerService = pickerService; + if (broadcastTextsParameter is IAsyncParameter asyncBroadcast) + this.broadcastTextsParameter = asyncBroadcast; + } + + public Task<(long, bool)> PickValue(long value, object context) + { + IParameter parameter = Parameter.Instance; + if (context is DatabaseEntity entity) + { + var cell = entity.GetTypedValueOrThrow("type"); + if (cell == (int)IMangosDatabaseProvider.RandomTemplateType.Text && broadcastTextsParameter != null) + { + parameter = broadcastTextsParameter; + } + else if (cell == (int)IMangosDatabaseProvider.RandomTemplateType.RelayScript) + { + // maybe in the future we might have something better here + parameter = Parameter.Instance; + } + } + + return pickerService.PickParameter(parameter, value); + } + + public string ToString(long value, DatabaseEntity context) + { + return value.ToString(); + } + + public Task ToStringAsync(long value, CancellationToken token, DatabaseEntity entity) + { + var cell = entity.GetTypedValueOrThrow("type"); + if (cell == (int)IMangosDatabaseProvider.RandomTemplateType.Text && broadcastTextsParameter != null) + return broadcastTextsParameter.ToStringAsync(value, token); + return Task.FromResult(value.ToString()); + } + + public string? Prefix => null; + public bool HasItems => true; + public bool AllowUnknownItems => true; + public string ToString(long value) => value.ToString(); + public Dictionary? Items => null; +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/FastGroupingWrapPanel.cs b/WDE.EventAiEditor.Avalonia/Editor/FastGroupingWrapPanel.cs new file mode 100644 index 000000000..c929f12a0 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/FastGroupingWrapPanel.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Utilities; +using Avalonia.VisualTree; +using WDE.EventAiEditor.Avalonia.Editor.Views; +using static System.Math; + +namespace WDE.EventAiEditor.Avalonia.Editor +{ + public class FastGroupingWrapPanel : Panel, INavigableContainer + { + private double itemWidth; + public static readonly DirectProperty ItemWidthProperty = AvaloniaProperty.RegisterDirect("ItemWidth", o => o.ItemWidth, (o, v) => o.ItemWidth = v); + private double itemHeight; + public static readonly DirectProperty ItemHeightProperty = AvaloniaProperty.RegisterDirect("ItemHeight", o => o.ItemHeight, (o, v) => o.ItemHeight = v); + + public static readonly AttachedProperty GroupProperty = + AvaloniaProperty.RegisterAttached("Group"); + + static FastGroupingWrapPanel() + { + AffectsParentMeasure(GroupProperty); + AffectsParentMeasure(IsVisibleProperty); + } + + public static string GetGroup(IControl control) + { + return control.GetValue(GroupProperty); + } + + public static void SetGroup(IControl control, string value) + { + control.SetValue(GroupProperty, value); + } + + public double ItemWidth + { + get => itemWidth; + set => SetAndRaise(ItemWidthProperty, ref itemWidth, value); + } + + public double ItemHeight + { + get => itemHeight; + set => SetAndRaise(ItemHeightProperty, ref itemHeight, value); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + var brush = new ImmutableSolidColorBrush(new Color(0xFF, 0x22, 0x6E, 0x8B)); + var pen = new ImmutablePen(brush, 1); + foreach (var group in groupStartHeight) + { + if (group.Value.height < float.Epsilon) + continue; + var lineY = group.Value.Item1 + 15; + var ft = new FormattedText(); + ft.FontSize = 12; + ft.Text = group.Key; + ft.Typeface = new Typeface(TextBlock.GetFontFamily(this), FontStyle.Normal, FontWeight.Bold); + context.DrawLine(pen, new Point(0, lineY), new Point(15, lineY)); + context.DrawText(brush, new Point(20, group.Value.Item1 + 16 - ft.Bounds.Height / 2), ft); + context.DrawLine(pen, new Point(20 + ft.Bounds.Width + 5, lineY), new Point(this.Bounds.Width, lineY)); + } + } + + private void BuildGrid() + { + if (!gridDirty) + return; + + var itemsPerRow = (int)Max(1, Floor(this.Bounds.Width / itemWidth)); + if (grid.GetLength(0) < itemsPerRow || grid.GetLength(1) < VisualChildren.Count) + grid = new IControl[(int)itemsPerRow, VisualChildren.Count]; + else + { + for (int i = 0; i < grid.GetLength(0); ++i) + { + for (int j = 0; j < grid.GetLength(1); ++j) + grid[i, j] = null; + } + } + + int y = 0; + foreach (var group in elementsInGroup) + { + int x = 0; + foreach (var item in group.Value) + { + grid[x, y] = item; + x++; + if (x == itemsPerRow) + { + x = 0; + y++; + } + } + + if (x != 0) + y++; + } + + gridDirty = false; + } + + Dictionary> elementsInGroup = new(); + Dictionary groupStartHeight = new(); + private IControl?[,] grid = new IControl[0, 0]; + private bool gridDirty = true; + protected override Size MeasureOverride(Size availableSize) + { + double width = 0; + double height = 0; + gridDirty = true; + + var visualChildren = VisualChildren; + var visualCount = visualChildren.Count; + + elementsInGroup.Clear(); + groupStartHeight.Clear(); + elementsInGroup["Favourites"] = new List() { }; + for (var i = 0; i < visualCount; i++) + { + IVisual visual = visualChildren[i]; + + if (visual is ILayoutable layoutable && visual is IControl control + && layoutable.IsVisible) + { + var group = GetGroup(control); + if (!elementsInGroup.ContainsKey(group)) + elementsInGroup[group] = new List() { control }; + else + elementsInGroup[group].Add(control); + } + } + + var itemsPerRow = Max(1, Floor(availableSize.Width / itemWidth)); + foreach (var group in elementsInGroup) + { + if (group.Value.Count > 0) + { + height += 32; // header + groupStartHeight[group.Key] = (height - 32, 0, height, 0); + height += itemHeight * Ceiling(group.Value.Count / itemsPerRow); + } + else + groupStartHeight[group.Key] = (0, 0, 0, 0); + } + + return new Size(Min(availableSize.Width, visualCount * itemWidth), height); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var arrangeRect = new Rect(finalSize); + + var visualChildren = VisualChildren; + var visualCount = visualChildren.Count; + + var itemsPerRow = Max(1, (int)Floor(finalSize.Width / itemWidth)); + + MeasureOverride(finalSize); + + for (var i = 0; i < visualCount; i++) + { + IVisual visual = visualChildren[i]; + + if (visual is ILayoutable layoutable && visual is IControl control && layoutable.IsVisible) + { + var group = GetGroup(control); + if (groupStartHeight.TryGetValue(group, out var startPos)) + { + double x = startPos.Item2; + double y = startPos.Item3; + layoutable.Arrange(new Rect(x, y, itemWidth, itemHeight)); + if (startPos.Item4 + 1 == itemsPerRow) + groupStartHeight[group] = (startPos.Item1, 0, y + itemHeight, 0); + else + groupStartHeight[group] = (startPos.Item1, x + itemWidth, y, startPos.Item4 + 1); + } + } + } + InvalidateVisual(); + return finalSize; + } + + private (int, int)? Find(IInputElement element) + { + BuildGrid(); + for (int i = 0; i < grid.GetLength(0); ++i) + { + for (int j = 0; j < grid.GetLength(1); ++j) + { + if (grid[i, j] == element) + { + return (i, j); + } + } + } + + return null; + } + + public IInputElement GetControl(NavigationDirection direction, IInputElement @from, bool wrap) + { + IControl control = (IControl)@from; + var index = Find(from); + if (index == null) + return from; + + switch (direction) + { + case NavigationDirection.Next: + case NavigationDirection.PageDown: + case NavigationDirection.PageUp: + case NavigationDirection.Last: + case NavigationDirection.First: + case NavigationDirection.Previous: + break; + case NavigationDirection.Left: + return GetElementInDir(index.Value, (-1, 0)) ?? from; + case NavigationDirection.Right: + return GetElementInDir(index.Value, (1, 0)) ?? from; + case NavigationDirection.Up: + return GetElementInDir(index.Value, (0, -1)) ?? from; + case NavigationDirection.Down: + return GetElementInDir(index.Value, (0, 1)) ?? from; + default: + throw new ArgumentOutOfRangeException(nameof(direction), direction, null); + } + return @from; + } + + private IInputElement? GetElementInDir((int x, int y) start, (int x, int y) dir) + { + BuildGrid(); + (int x, int y) cur = (start.x + dir.x, start.y + dir.y); + while (cur.x >= 0 && cur.x < grid.GetLength(0) && + cur.y >= 0 && cur.y < grid.GetLength(1) && + grid[cur.x, cur.y] == null) + cur = (cur.x + dir.x, cur.y + dir.y); + + if (cur.x >= 0 && cur.x < grid.GetLength(0) && + cur.y >= 0 && cur.y < grid.GetLength(1)) + return grid[cur.x, cur.y]; + return null; + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/README.md b/WDE.EventAiEditor.Avalonia/Editor/README.md new file mode 100644 index 000000000..b3ee9ea83 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/README.md @@ -0,0 +1 @@ +Code in this folder is absolutely against any good WPF and MVVM practises, if you know how to write editor in a better way - any refactor (or rather - writing from the scratch) is welcome! \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiActionView.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiActionView.cs new file mode 100644 index 000000000..4b2775a86 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiActionView.cs @@ -0,0 +1,74 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using WDE.Common.Avalonia.Controls; +using WDE.EventAiEditor.Models; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + /// + /// Interaction logic for EventAiActionView.xaml + /// + public class EventAiActionView : SelectableTemplatedControl + { + public static AvaloniaProperty DeselectAllButActionsRequestProperty = + AvaloniaProperty.Register(nameof(DeselectAllButActionsRequest)); + + public static AvaloniaProperty EditActionCommandProperty = + AvaloniaProperty.Register(nameof(EditActionCommand)); + + public static AvaloniaProperty DirectEditParameterProperty = + AvaloniaProperty.Register(nameof(DirectEditParameter)); + + private int indent; + public static readonly DirectProperty IndentProperty + = AvaloniaProperty.RegisterDirect("Indent", o => o.Indent, (o, v) => o.Indent = v); + + public ICommand DeselectAllButActionsRequest + { + get => (ICommand) GetValue(DeselectAllButActionsRequestProperty); + set => SetValue(DeselectAllButActionsRequestProperty, value); + } + + public ICommand EditActionCommand + { + get => (ICommand) GetValue(EditActionCommandProperty); + set => SetValue(EditActionCommandProperty, value); + } + + public ICommand DirectEditParameter + { + get => (ICommand) GetValue(DirectEditParameterProperty); + set => SetValue(DirectEditParameterProperty, value); + } + + public int Indent + { + get { return indent; } + set { SetAndRaise(IndentProperty, ref indent, value); } + } + + protected override void DeselectOthers() + { + DeselectAllButActionsRequest?.Execute(null); + } + + protected override void OnDirectEdit(object context) + { + DirectEditParameter?.Execute(context); + } + + protected override void OnEdit() + { + EditActionCommand?.Execute(DataContext); + } + + protected override void OnDataContextEndUpdate() + { + var isComment = DataContext is EventAiAction action && action.Id == EventAiConstants.ActionComment; + PseudoClasses.Set(":comment", isComment); + PseudoClasses.Set(":action", !isComment); + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventFlagsView.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventFlagsView.cs new file mode 100644 index 000000000..85b374282 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventFlagsView.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Metadata; +using WDE.MVVM; +using WDE.MVVM.Observable; +using WDE.EventAiEditor.Models; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + public class EventAiEventFlagsView : TemplatedControl + { + private IDisposable? flagsDisposable; + private AvaloniaList flags = new(); + public static readonly DirectProperty> FlagsProperty = AvaloniaProperty.RegisterDirect>("Flags", o => o.Flags, (o, v) => o.Flags = v); + + public AvaloniaList Flags + { + get => flags; + set => SetAndRaise(FlagsProperty, ref flags, value); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + if (DataContext is EventAiEvent se) + { + var flagsObservable = se.Flags.ToObservable(t => t.Value); + var phasesObservable = se.Phases.ToObservable(t => t.Value); + var combined = flagsObservable.CombineLatest(phasesObservable, (flags, phases) => (flags, phases)); + flagsDisposable = combined.SubscribeAction(tuple => + { + flags.Clear(); + var (eventFlagsNum, eventPhasesNum) = tuple; + + if (eventFlagsNum > 0 && se.Flags.Parameter.HasItems) + { + foreach (var item in se.Flags.Parameter.Items!) + { + if (item.Key == 0) + continue; + + if ((eventFlagsNum & item.Key) > 0) + flags.Add(new IconViewModel(FlagToIconTextSymbol(item.Key), item.Value.Name, item.Value.Description)); + } + } + + if (eventPhasesNum > 0 && se.Phases.Parameter.HasItems) + { + int totalPhases = se.Phases.Parameter.Items! + .Count(item => (eventPhasesNum & item.Key) > 0); + + var currentPhases = se.Phases.Parameter.Items! + .Where(item => item.Key != 0 && (eventPhasesNum & item.Key) > 0) + .Select(item => PhaseMaskToPhase((int) item.Key)); + + if (totalPhases > 3) + { + flags.Add(new IconViewModel(currentPhases)); + } + else + { + foreach (var phase in currentPhases) + flags.Add(new IconViewModel(phase)); + } + } + }); + } + } + + private static string FlagToIconTextSymbol(long flag) + { + EventAiFlag f = (EventAiFlag) flag; + if (f == EventAiFlag.Repeatable) + return "🔁"; + if (f == EventAiFlag.DebugOnly) + return "🐛"; + if (f == EventAiFlag.CombatAction) + return "🛡️"; + if (f == EventAiFlag.RandomAction) + return "🎲"; + if (f == EventAiFlag.MeleeModeOnly) + return "🗡"; + if (f == EventAiFlag.RangedModeOnly) + return "🏹"; + if (f == EventAiFlag.Difficulty0) + return "A"; + if (f == EventAiFlag.Difficulty1) + return "B"; + if (f == EventAiFlag.Difficulty2) + return "C"; + if (f == EventAiFlag.Difficulty3) + return "D"; + return "?"; + } + + private static int PhaseMaskToPhase(int phaseMask) + { + for (int i = 0; i < 31; ++i) + { + if (((1 << i) & phaseMask) > 0) + return i + 1; + } + + return 0; + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + flagsDisposable?.Dispose(); + flagsDisposable = null; + } + + public struct IconViewModel + { + public bool IsPhaseFlag { get; set; } + public string Text { get; set; } + public string ToolTip { get; set; } + + public IconViewModel(long phase) + { + IsPhaseFlag = true; + Text = phase.ToString(); + ToolTip = "Event will NOT activate in script phase " + phase; + } + + public IconViewModel(IEnumerable phases) + { + IsPhaseFlag = true; + Text = "..."; + ToolTip = "Event will NOT activate in script phases: " + string.Join(", ", phases); + } + + public IconViewModel(string text, string flagName, string? tooltip) + { + Text = text; + ToolTip = tooltip != null ? $"Event flag {flagName}: {tooltip}" : $"Event flag {flagName}"; + IsPhaseFlag = false; + } + } + } + + public class EventFlagPhaseDataSelector : IDataTemplate + { + [TemplateContent] + public object? PhaseView { get; set; } + + [TemplateContent] + public object? FlagView { get; set; } + + public IControl Build(object param) + { + if (param is EventAiEventFlagsView.IconViewModel {IsPhaseFlag: true}) + return TemplateContent.Load(PhaseView).Control; + return TemplateContent.Load(FlagView).Control; + } + + public bool Match(object data) + { + return true; + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventView.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventView.cs new file mode 100644 index 000000000..6df923763 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiEventView.cs @@ -0,0 +1,73 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using WDE.Common.Avalonia.Controls; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + /// + /// Interaction logic for EventAiEventView.xaml + /// + public class EventAiEventView : SelectableTemplatedControl + { + public static readonly DirectProperty EditEventCommandProperty = + AvaloniaProperty.RegisterDirect( + nameof(EditEventCommand), + o => o.EditEventCommand, + (o, v) => o.EditEventCommand = v); + + public static readonly DirectProperty DeselectActionsOfDeselectedEventsRequestProperty = + AvaloniaProperty.RegisterDirect( + nameof(DeselectActionsOfDeselectedEventsRequest), + o => o.DeselectActionsOfDeselectedEventsRequest, + (o, v) => o.DeselectActionsOfDeselectedEventsRequest = v); + + + public static readonly DirectProperty DirectEditParameterProperty = + AvaloniaProperty.RegisterDirect( + nameof(DirectEditParameter), + o => o.DirectEditParameter, + (o, v) => o.DirectEditParameter = v); + + private ICommand? editEventCommand; + public ICommand? EditEventCommand + { + get => editEventCommand; + set => SetAndRaise(EditEventCommandProperty, ref editEventCommand, value); + } + + private ICommand? deselectActionsOfDeselectedEventsRequest; + public ICommand? DeselectActionsOfDeselectedEventsRequest + { + get => deselectActionsOfDeselectedEventsRequest; + set => SetAndRaise(DeselectActionsOfDeselectedEventsRequestProperty, ref deselectActionsOfDeselectedEventsRequest, value); + } + + private ICommand? directEditParameter; + public ICommand? DirectEditParameter + { + get => directEditParameter; + set => SetAndRaise(DirectEditParameterProperty, ref directEditParameter, value); + } + + protected override void OnEdit() + { + EditEventCommand?.Execute(DataContext); + } + + protected override void OnDirectEdit(object context) + { + DirectEditParameter?.Execute(context); + } + + protected override void DeselectOthers() + { + DeselectActionsOfDeselectedEventsRequest?.Execute(null); + } + + public static readonly AvaloniaProperty SelectedProperty = AvaloniaProperty.RegisterAttached("Selected"); + public static bool GetSelected(IControl control) => (bool)control.GetValue(SelectedProperty); + public static void SetSelected(IControl control, bool value) => control.SetValue(SelectedProperty, value); + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiPanelLayout.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiPanelLayout.cs new file mode 100644 index 000000000..3b12c58c9 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiPanelLayout.cs @@ -0,0 +1,646 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Media; +using WDE.Common.Managers; +using WDE.EventAiEditor.Editor.UserControls; +using WDE.EventAiEditor.Editor.ViewModels; +using WDE.EventAiEditor.Models; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + internal class EventAiPanelLayout : Panel + { + private readonly List<(float y, float height, int actionIndex, int eventIndex)> actionHeights = new(); + private readonly List<(float y, float height, int conditionIndex, int eventIndex)> conditionHeights = new(); + private readonly List<(float y, float height, int eventIndex)> eventHeights = new(); + + private readonly Dictionary actionToPresenter = new(); + private readonly Dictionary eventToPresenter = new(); + private readonly Dictionary presenterToAction = new(); + private readonly Dictionary presenterToEvent = new(); + + private ContentPresenter? addActionPresenter; + private NewActionViewModel? addActionViewModel; + + private bool draggingActions; + private bool draggingEvents; + private bool isCopying; + + private Point mouseStartPosition; + private float mouseY; + + public (float y, float height, int eventIndex) OverIndexEvent { get; set; } + private (float y, float height, int actionIndex, int eventIndex) overIndexAction; + + private static double PaddingLeft = 20; + + private double EventWidth(double totalWidth) => Math.Min(Math.Max(totalWidth - 50, PaddingLeft + 10), 250); + + static EventAiPanelLayout() + { + AffectsRender(ProblemsProperty); + PointerPressedEvent.AddClassHandler(PointerPressedHandled, RoutingStrategies.Tunnel, true); + } + + private static void PointerPressedHandled(EventAiPanelLayout panel, PointerPressedEventArgs e) + { + panel.mouseStartPosition = e.GetPosition(panel); + } + + protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.LogicalChildrenCollectionChanged(sender, e); + if (e.Action == NotifyCollectionChangedAction.Add) + { + foreach (var o in e.NewItems!) + { + OnVisualChildrenChanged(o as ContentPresenter, null); + } + } + else if (e.Action == NotifyCollectionChangedAction.Remove) + { + foreach (var o in e.OldItems!) + { + OnVisualChildrenChanged(null, o as ContentPresenter); + } + } + + InvalidateArrange(); + InvalidateVisual(); + InvalidateMeasure(); + } + + protected void OnVisualChildrenChanged(ContentPresenter? visualAdded, ContentPresenter? visualRemoved) + { + if (visualAdded is ContentPresenter visualAddedPresenter) + { + visualAddedPresenter.DataContextChanged += OnLoadVisualChild; + OnLoadVisualChild(visualAddedPresenter, EventArgs.Empty); + } + + if (visualRemoved is ContentPresenter visualRemovedPresenter) + { + visualRemovedPresenter.DataContextChanged -= OnLoadVisualChild; + if (visualRemovedPresenter.Content is EventAiEvent @event) + { + presenterToEvent.Remove(visualRemovedPresenter); + eventToPresenter.Remove(@event); + } + else if (visualRemovedPresenter.Content is EventAiAction action) + { + presenterToAction.Remove(visualRemovedPresenter); + actionToPresenter.Remove(action); + } + else if (visualRemovedPresenter.Content is NewActionViewModel) + { + addActionPresenter = null; + addActionViewModel = null; + } + } + + InvalidateArrange(); + InvalidateVisual(); + InvalidateMeasure(); + } + + private void OnLoadVisualChild(object? sender, EventArgs e) + { + if (sender is not ContentPresenter visualAddedPresenter) + return; + + if (visualAddedPresenter.Content is EventAiEvent @event) + { + presenterToEvent[visualAddedPresenter] = @event; + eventToPresenter[@event] = visualAddedPresenter; + } + else if (visualAddedPresenter.Content is EventAiAction action) + { + presenterToAction[visualAddedPresenter] = action; + actionToPresenter[action] = visualAddedPresenter; + } + else if (visualAddedPresenter.Content is NewActionViewModel vm) + { + addActionPresenter = visualAddedPresenter; + addActionViewModel = vm; + } + InvalidateArrange(); + InvalidateVisual(); + InvalidateMeasure(); + } + + private IEnumerable Events() + { + foreach (ContentPresenter child in Children) + { + if (child.Content is EventAiEvent) + yield return child; + } + } + + protected override Size MeasureOverride(Size availableSize) + { + float totalDesiredHeight = 0; + double eventWidth = EventWidth((float) availableSize.Width); + double actionWidth = availableSize.Width - eventWidth; + + foreach (ContentPresenter eventPresenter in Events()) + { + eventPresenter.Measure(new Size(eventWidth - PaddingLeft, availableSize.Height)); + + float actionsHeight = 26; + + if (!presenterToEvent.ContainsKey(eventPresenter)) + continue; + + EventAiEvent eventAiEvent = presenterToEvent[eventPresenter]; + foreach (EventAiAction action in eventAiEvent.Actions) + { + if (!actionToPresenter.TryGetValue(action, out var actionPresenter)) + continue; + + actionPresenter.Measure(new Size(actionWidth, availableSize.Height)); + + actionsHeight += (float) actionPresenter.DesiredSize.Height + ActionSpacing; + } + + totalDesiredHeight += Math.Max(actionsHeight, (float) eventPresenter.DesiredSize.Height) + EventSpacing; + } + + return new Size(availableSize.Width, Math.Max(0, totalDesiredHeight)); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + UpdateIsCopying(e.KeyModifiers); + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && + e.ClickCount == 1) + { + foreach (ContentPresenter @event in Children) + if (@event.Child != null) + SetSelected(@event.Child, false); + } + } + + private void UpdateIsCopying(KeyModifiers key) + { + var systemWideControlModifier = AvaloniaLocator.Current + .GetService()?.CommandModifiers ?? KeyModifiers.Control; + isCopying = key.HasFlagFast(systemWideControlModifier); + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + var systemWideControlModifier = AvaloniaLocator.Current + .GetService()?.CommandModifiers ?? KeyModifiers.Control; + + base.OnPointerReleased(e); + UpdateIsCopying(e.KeyModifiers); + StopDragging(); + } + + private bool AnythingSelected() + { + foreach (var e in Script.Events) + { + if (e.IsSelected) + return true; + } + foreach (var e in Script.Events) + { + foreach (var a in e.Actions) + if (a.IsSelected) + return true; + } + + return false; + } + + private void StopDragging() + { + if (AnythingSelected()) + { + if (draggingEvents) + DropItems?.Execute(new DropActionsConditionsArgs{EventIndex = OverIndexEvent.eventIndex, ActionIndex = 0, Copy = isCopying}); + else if (draggingActions) + { + DropActions?.Execute(new DropActionsConditionsArgs + {EventIndex = overIndexAction.eventIndex, ActionIndex = overIndexAction.actionIndex, Copy = isCopying}); + } + } + + draggingEvents = false; + draggingActions = false; + InvalidateArrange(); + InvalidateVisual(); + } + + private static FormattedText? vvvvText; + private static FormattedTextNumberCache NumberCache = new(); + public override void Render(DrawingContext dc) + { + base.Render(dc); + if (AnythingSelected()) + { + if (draggingActions) + { + double x = EventWidth(Bounds.Width); + float y = overIndexAction.y - overIndexAction.height / 2 - 1; + dc.DrawLine(new Pen(Brushes.Gray, 1), new Point(x, y), new Point(x + 200, y)); + } + else if (draggingEvents) + { + if (isCopying) + { + float x = 1; + float y = OverIndexEvent.y - OverIndexEvent.height / 2 - 1; + dc.DrawLine(new Pen(Brushes.Gray, 1), new Point(x, y), new Point(x + EventWidth(Bounds.Width), y)); + } + } + } + + if (vvvvText == null) + { + vvvvText = new FormattedText(); + vvvvText.FontSize = 7; + vvvvText.Text = "vvvv"; + vvvvText.Typeface = Typeface.Default; + } + + int index = 1; + double yPos = 0; + foreach (var e in Script.Events) + { + if (e.Actions.Count == 0) + { + if (!eventToPresenter.TryGetValue(e, out var eventPresenter)) + continue; + yPos = eventPresenter.Bounds.Y; + + var ft = NumberCache.Get(index); + dc.DrawText(Brushes.DarkGray, new Point(0, yPos + 5), ft); + DrawProblems(dc, index, yPos); + index++; + } + else + { + foreach (var a in e.Actions) + { + if (!actionToPresenter.TryGetValue(a, out var actionPresenter)) + continue; + + yPos = actionPresenter.Bounds.Y; + + var ft = NumberCache.Get(index); + dc.DrawText(Brushes.DarkGray, new Point(0, yPos + 5), ft); + DrawProblems(dc, index, yPos); + index++; + } + } + } + } + + private void DrawProblems(DrawingContext dc, int index, double yPos) + { + if (Problems != null && Problems.TryGetValue(index, out var severity)) + { + dc.DrawText(severity is DiagnosticSeverity.Error or DiagnosticSeverity.Critical ? Brushes.Red : Brushes.Orange, new Point(0, yPos + 5 + 10), vvvvText); + } + } + + public class FormattedTextNumberCache + { + private FormattedText[] cache = new FormattedText[0]; + + public FormattedTextNumberCache() + { + + } + + public FormattedText Get(int index) + { + if (cache.Length <= index) + EnsureCache(index + 1); + return cache[index]; + } + + private void EnsureCache(int size) + { + int old = cache.Length; + size = Math.Max(size, cache.Length * 2 + 1); + Array.Resize(ref cache, size); + for (int i = old; i < size; ++i) + { + cache[i] = new FormattedText(); + cache[i].Text = $"{i}"; + cache[i].FontSize = 10; + cache[i].Typeface = Typeface.Default; + } + } + } + + private bool AnyActionSelected() + { + return actionToPresenter.Keys.Any(a => a.IsSelected); + } + + private bool AnyEventSelected() + { + return eventToPresenter.Keys.Any(a => a.IsSelected); + } + + private bool mouseStartPositionValid = false; + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + UpdateIsCopying(e.KeyModifiers); + var state = e.GetCurrentPoint(this); + + mouseY = (float) e.GetPosition(this).Y; + if (!state.Properties.IsLeftButtonPressed) + { + mouseStartPosition = e.GetPosition(this); + mouseStartPositionValid = true; + } + if (mouseStartPositionValid && + state.Properties.IsLeftButtonPressed && + !draggingActions && !draggingEvents) + { + var d = new Point(mouseStartPosition.X - e.GetPosition(this).X, + mouseStartPosition.Y - e.GetPosition(this).Y); + var dist = d.X * d.X + d.Y * d.Y; + if (dist > 10 * 10) + { + if (e.GetPosition(this).X < EventWidth(Bounds.Width)) + { + if (AnyEventSelected()) + draggingEvents = true; + } + else + draggingActions = AnyActionSelected(); + + if (draggingEvents || draggingActions) + { + mouseStartPositionValid = false; + //CaptureMouse(); + } + } + } + + if (draggingEvents) + { + var eventIndex = 0; + var found = false; + foreach (var tuple in eventHeights) + { + if (tuple.y > mouseY) + { + OverIndexEvent = tuple; + found = true; + break; + } + + eventIndex++; + } + + if (!found && eventHeights.Count >= 1) + { + OverIndexEvent = eventHeights[^1];//eventHeights.Count > 0 && mouseY > eventHeights[^1].y + //? eventHeights[^1] + //: eventHeights[0]; + } + } + else if (draggingActions) + { + foreach ((float y, float height, int actionIndex, int eventIndex) tuple in actionHeights) + { + if (tuple.y > mouseY) + { + overIndexAction = tuple; + break; + } + } + InvalidateVisual(); + } + + InvalidateArrange(); + } + + protected override void OnPointerLeave(PointerEventArgs e) + { + base.OnPointerLeave(e); + mouseStartPositionValid = false; + } + + protected override Size ArrangeOverride(Size finalSize) + { + eventHeights.Clear(); + actionHeights.Clear(); + conditionHeights.Clear(); + float y = EventSpacing; + + if (draggingActions || draggingEvents) + { + addActionPresenter?.Arrange(new Rect(-5, -5, 1, 1)); + } + + float lastHeight = 0; + float selectedHeight = 0; + int eventIndex = 0; + foreach (EventAiEvent ev in Script.Events) + { + eventIndex++; + if (!eventToPresenter.TryGetValue(ev, out var eventPresenter)) + continue; + + lastHeight = Math.Max(MeasureActions(eventPresenter), (float) eventPresenter.DesiredSize.Height); + eventHeights.Add((y + (lastHeight + EventSpacing) / 2, lastHeight + EventSpacing, eventIndex - 1)); + y += lastHeight + EventSpacing; + + if (!draggingEvents || isCopying || !GetSelected(eventPresenter.Child)) + continue; + selectedHeight += lastHeight + EventSpacing; + } + + eventHeights.Add((y, 0, eventIndex)); + + y = EventSpacing; + float start = 0; + if (OverIndexEvent.eventIndex == 0) + { + start = y; + y += selectedHeight; + } + + eventIndex = 0; + if (addActionViewModel != null) + addActionViewModel.Event = null; + + foreach (EventAiEvent ev in Script.Events) + { + if (!eventToPresenter.TryGetValue(ev, out var eventPresenter)) + continue; + + var height = (float)eventPresenter.DesiredSize.Height; + if (!draggingEvents || isCopying || !GetSelected(eventPresenter.Child)) + { + float actionHeight = ArrangeActions(eventIndex, 0, finalSize, eventPresenter, y, height); + height = Math.Max(height, actionHeight); + eventPresenter.Arrange(new Rect(PaddingLeft, y, EventWidth(finalSize.Width) - PaddingLeft, height)); + + if (mouseY > y && mouseY < y + height && !draggingActions && !draggingEvents) + { + if (presenterToEvent.TryGetValue(eventPresenter, out EventAiEvent? @event) && @event != null) + { + if (addActionViewModel != null) + addActionViewModel.Event = @event; + } + + addActionPresenter?.Arrange(new Rect(EventWidth(finalSize.Width), + y + actionHeight - 26, + Math.Max(finalSize.Width - EventWidth(finalSize.Width), 0), + 24)); + } + + y += height + EventSpacing; + } + + eventIndex++; + if (eventIndex == OverIndexEvent.eventIndex) + { + start = y; + y += selectedHeight; + } + } + + eventIndex = 0; + foreach (EventAiEvent ev in Script.Events) + { + if (!eventToPresenter.TryGetValue(ev, out var eventPresenter)) + continue; + var height = (float) eventPresenter.DesiredSize.Height; + if (draggingEvents && !isCopying && GetSelected(eventPresenter.Child)) + { + height = Math.Max(height, ArrangeActions(eventIndex, 20, finalSize, eventPresenter, start, height)); + eventPresenter.Arrange(new Rect(20 + PaddingLeft, start, EventWidth(finalSize.Width) - PaddingLeft, height)); + start += height + EventSpacing; + } + + eventIndex++; + } + return finalSize; + } + + private float ArrangeActions(int eventIndex, + float x, + Size totalSize, + ContentPresenter eveentPresenter, + float y, + float eventHeight) + { + float totalHeight = 26; + if (!presenterToEvent.TryGetValue(eveentPresenter, out EventAiEvent? @event)) + return totalHeight; + + var actionIndex = 0; + foreach (EventAiAction action in @event.Actions) + { + if (!actionToPresenter.TryGetValue(action, out ContentPresenter? actionPresenter)) + continue; + + var height = (float) actionPresenter.DesiredSize.Height; + actionPresenter.Arrange(new Rect(EventWidth(totalSize.Width) + x, y, Math.Max(totalSize.Width - EventWidth(totalSize.Width), 0), height)); + actionHeights.Add((y + (height + ActionSpacing) / 2, height + ActionSpacing, actionIndex, eventIndex)); + y += height + ActionSpacing; + + totalHeight += height + ActionSpacing; + actionIndex++; + } + + float rest = Math.Max(26, eventHeight - (totalHeight - 26)); + + actionHeights.Add((y + (rest + ActionSpacing) / 2, rest, actionIndex, eventIndex)); + + return totalHeight; + } + + private float MeasureActions(ContentPresenter eventPresenter) + { + float totalHeight = 26; + if (!presenterToEvent.TryGetValue(eventPresenter, out EventAiEvent? @event)) + return totalHeight; + + foreach (EventAiAction action in @event.Actions) + { + if (!actionToPresenter.TryGetValue(action, out ContentPresenter? actionPresenter)) + continue; + + var height = (float) actionPresenter.DesiredSize.Height; + totalHeight += height + ActionSpacing; + } + + return totalHeight; + } + + // protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + // { + // base.OnRenderSizeChanged(sizeInfo); + // InvalidateMeasure(); + // InvalidateArrange(); + // } + // + + public EventAiScript Script + { + get => (EventAiScript) GetValue(ScriptProperty); + set => SetValue(ScriptProperty, value); + } + + public static readonly AvaloniaProperty ScriptProperty = + AvaloniaProperty.Register(nameof(Script), null); + + public Dictionary? Problems + { + get => (Dictionary?) GetValue(ProblemsProperty); + set => SetValue(ProblemsProperty, value); + } + public static readonly AvaloniaProperty ProblemsProperty = + AvaloniaProperty.Register?>(nameof(Problems)); + + public float EventSpacing => 10; + + public float ActionSpacing => 2; + + public static readonly AvaloniaProperty SelectedProperty = AvaloniaProperty.RegisterAttached("Selected"); + public static bool GetSelected(IControl control) => (bool)control.GetValue(SelectedProperty); + public static void SetSelected(IControl control, bool value) => control.SetValue(SelectedProperty, value); + + public static readonly AvaloniaProperty DropItemsProperty = AvaloniaProperty.Register(nameof(DropItems)); + + public ICommand DropItems + { + get => (ICommand) GetValue(DropItemsProperty); + set => SetValue(DropItemsProperty, value); + } + + public static readonly AvaloniaProperty DropActionsProperty = AvaloniaProperty.Register(nameof(DropActions)); + + public ICommand DropActions + { + get => (ICommand) GetValue(DropActionsProperty); + set => SetValue(DropActionsProperty, value); + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml new file mode 100644 index 000000000..99aec5f7c --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml @@ -0,0 +1,111 @@ + + + False + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml.cs new file mode 100644 index 000000000..d5355d6ea --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/EventAiView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + /// + /// Interaction logic for EventAiView.xaml + /// + public partial class EventAiView : UserControl + { + public EventAiView() + { + InitializeComponent(); + } + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/MiniEventIcon.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/MiniEventIcon.cs new file mode 100644 index 000000000..60c9e5dba --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/MiniEventIcon.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + public class MiniEventIcon : TemplatedControl + { + private string text = ""; + public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect("Text", o => o.Text, (o, v) => o.Text = v); + + public static readonly DirectProperty IsNotEmojiProperty = AvaloniaProperty.RegisterDirect("IsNotEmoji", o => o.IsNotEmoji); + public bool IsNotEmoji => string.IsNullOrEmpty(Text) || Text[0] <= 127; + + public string Text + { + get => text; + set + { + SetAndRaise(TextProperty, ref text, value); + RaisePropertyChanged(IsNotEmojiProperty, Optional.Empty, IsNotEmoji); + } + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/UserControls/SelectableTemplatedControl.cs b/WDE.EventAiEditor.Avalonia/Editor/UserControls/SelectableTemplatedControl.cs new file mode 100644 index 000000000..f2eb628e8 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/UserControls/SelectableTemplatedControl.cs @@ -0,0 +1,118 @@ +using System; +using System.Windows.Input; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Input.Platform; +using WDE.Common.Avalonia.Controls; + +namespace WDE.EventAiEditor.Avalonia.Editor.UserControls +{ + public abstract class SelectableTemplatedControl : TemplatedControl + { + public static KeyModifiers MultiselectGesture { get; } = AvaloniaLocator.Current + .GetService()?.CommandModifiers ?? KeyModifiers.Control; + + public static readonly AvaloniaProperty DeselectAllRequestProperty = + AvaloniaProperty.Register(nameof(DeselectAllRequest)); + + public ICommand DeselectAllRequest + { + get => (ICommand) GetValue(DeselectAllRequestProperty); + set => SetValue(DeselectAllRequestProperty, value); + } + public static readonly DirectProperty IsSelectedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsSelected), + o => o.IsSelected, + (o, v) => o.IsSelected = v); + + private bool isSelected; + public bool IsSelected + { + get => isSelected; + set => SetAndRaise(IsSelectedProperty, ref isSelected, value); + } + + private bool IsMultiSelect(KeyModifiers modifiers) + { + return modifiers.HasFlagFast(MultiselectGesture); + } + + private ulong lastPressedTimestamp = 0; + private int lastClickCount = 0; + private bool lastPressedWithControlOn = false; + private Point pressPosition; + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + lastPressedTimestamp = e.Timestamp; + lastClickCount = e.ClickCount; + lastPressedWithControlOn = IsMultiSelect(e.KeyModifiers); + pressPosition = e.GetPosition(this); + + if (e.ClickCount == 1) + { + if (e.Source is FormattedTextBlock tb && tb.OverContext != null) + return; + + if (!lastPressedWithControlOn) + DeselectAllRequest?.Execute(null); + else if (!IsSelected) + DeselectOthers(); + + if (!lastPressedWithControlOn && !IsSelected) + IsSelected = true; + e.Handled = true; + } + } + + protected abstract void DeselectOthers(); + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (lastClickCount == 1 && (e.Timestamp - lastPressedTimestamp) <= 1000) + { + if (e.Source is FormattedTextBlock tb && tb.OverContext != null) + { + OnDirectEdit(tb.OverContext); + e.Handled = true; + } + else + { + if (lastPressedWithControlOn) + { + var vector = pressPosition - e.GetPosition(this); + var dist = Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y); + if (dist < 5) + { + DeselectOthers(); + IsSelected = !IsSelected; + e.Handled = true; + } + } + } + } + if (lastClickCount == 2 && (e.Timestamp - lastPressedTimestamp) <= 1000) + { + OnEdit(); + e.Handled = true; + } + } + + protected virtual void OnEdit() {} + protected virtual void OnDirectEdit(object context) {} + + static SelectableTemplatedControl() + { + IsSelectedProperty.Changed.AddClassHandler((control, args) => + { + if (args.NewValue is bool b) + control.PseudoClasses.Set(":selected", b); + }); + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParameterEditorView.cs b/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParameterEditorView.cs new file mode 100644 index 000000000..bc7631ec0 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParameterEditorView.cs @@ -0,0 +1,48 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using AvaloniaStyles.Controls; +using WDE.EventAiEditor.Editor.ViewModels.Editing; + +namespace WDE.EventAiEditor.Avalonia.Editor.Views.Editing +{ + /// + /// Interaction logic for ParameterEditorView + /// + public class ParameterEditorView : TemplatedControl + { + public static readonly AttachedProperty OnEnterPressedProperty = + AvaloniaProperty.RegisterAttached("OnEnterPressed", typeof(ParameterEditorView)); + + private bool specialCopying; + public static readonly DirectProperty SpecialCopyingProperty = AvaloniaProperty.RegisterDirect("SpecialCopying", o => o.SpecialCopying, (o, v) => o.SpecialCopying = v); + + static ParameterEditorView() + { + OnEnterPressedProperty.Changed.AddClassHandler((box, args) => + { + box.OnEnterPressed += (sender, pressedArgs) => + { + var box = (CompletionComboBox)sender!; + if (pressedArgs.SelectedItem == null && long.TryParse(pressedArgs.SearchText, out var l)) + box.SelectedItem = new ParameterOption(l, "(unknown)"); + }; + }); + } + + public bool SpecialCopying + { + get => specialCopying; + set => SetAndRaise(SpecialCopyingProperty, ref specialCopying, value); + } + + public static bool GetOnEnterPressed(IAvaloniaObject obj) + { + return obj.GetValue(OnEnterPressedProperty); + } + + public static void SetOnEnterPressed(IAvaloniaObject obj, bool value) + { + obj.SetValue(OnEnterPressedProperty, value); + } + } +} \ No newline at end of file diff --git a/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParametersEditView.axaml b/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParametersEditView.axaml new file mode 100644 index 000000000..e9cdb1187 --- /dev/null +++ b/WDE.EventAiEditor.Avalonia/Editor/Views/Editing/ParametersEditView.axaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +