From 0e7de0a5f30cc4605ad6e7ca4312fc41350807ac Mon Sep 17 00:00:00 2001 From: Boki Date: Fri, 6 Mar 2026 14:37:05 -0500 Subject: [PATCH 1/5] refactor --- Automata.sln => Nexus.sln | 149 +++--- imgui.ini | 15 + src/Automata.Bot/Automata.Bot.csproj | 17 - .../Automata.Inventory.csproj | 13 - .../AtlasExecutor.cs | 12 +- .../BotOrchestrator.cs | 16 +- .../CombatManager.cs | 8 +- .../CraftingExecutor.cs | 8 +- .../DiamondExecutor.cs | 12 +- .../FlaskManager.cs | 6 +- .../GameExecutor.cs | 10 +- .../KulemakExecutor.cs | 14 +- .../MappingExecutor.cs | 14 +- src/Nexus.Bot/Nexus.Bot.csproj | 17 + .../ScrapExecutor.cs | 12 +- .../TradeExecutor.cs | 12 +- src/{Automata.Bot => Nexus.Bot}/TradeQueue.cs | 4 +- .../ActionQueue.cs | 2 +- src/{Roboto.Core => Nexus.Core}/Actions.cs | 2 +- src/{Roboto.Core => Nexus.Core}/BotConfig.cs | 2 +- src/{Roboto.Core => Nexus.Core}/Buff.cs | 2 +- .../CharacterProfile.cs | 2 +- .../CombatSettings.cs | 2 +- .../ConfigStore.cs | 2 +- src/{Automata.Core => Nexus.Core}/Delays.cs | 2 +- .../EntitySnapshot.cs | 2 +- src/{Roboto.Core => Nexus.Core}/Enums.cs | 2 +- .../FlaskSettings.cs | 2 +- src/{Roboto.Core => Nexus.Core}/FlaskState.cs | 2 +- src/{Roboto.Core => Nexus.Core}/GameState.cs | 2 +- .../GroundEffect.cs | 2 +- src/{Automata.Core => Nexus.Core}/Helpers.cs | 2 +- .../IInputController.cs | 2 +- src/{Roboto.Core => Nexus.Core}/ISystem.cs | 2 +- .../LinkManager.cs | 2 +- src/{Automata.Core => Nexus.Core}/Logging.cs | 2 +- .../ModPoolService.cs | 2 +- .../Nexus.Core.csproj} | 2 +- .../PlayerState.cs | 2 +- .../Poe2ScoutClient.cs | 2 +- .../ProfileManager.cs | 2 +- src/{Roboto.Core => Nexus.Core}/QuestInfo.cs | 2 +- .../QuestProgress.cs | 2 +- .../RePoETypes.cs | 2 +- .../SkillProfile.cs | 2 +- src/{Roboto.Core => Nexus.Core}/SkillState.cs | 2 +- .../StashCalibration.cs | 2 +- .../TargetSelection.cs | 2 +- .../TerrainQuery.cs | 2 +- src/{Roboto.Core => Nexus.Core}/ThreatMap.cs | 2 +- src/{Automata.Core => Nexus.Core}/Types.cs | 2 +- .../UiQuestInfo.cs | 2 +- .../WalkabilitySnapshot.cs | 2 +- .../WorldToScreen.cs | 2 +- src/{Roboto.Data => Nexus.Data}/AreaGraph.cs | 2 +- .../AreaNameLookup.cs | 2 +- .../EntityClassifier.cs | 4 +- .../EntityMapper.cs | 6 +- .../GameDataCache.cs | 6 +- .../GameStateEnricher.cs | 4 +- .../MemoryPoller.cs | 10 +- .../Nexus.Data.csproj} | 4 +- .../ClipboardHelper.cs | 2 +- .../GameController.cs | 4 +- .../IGameController.cs | 2 +- .../InputSender.cs | 4 +- .../Nexus.Game.csproj} | 2 +- .../WindowManager.cs | 2 +- .../Components/Actor.cs | 2 +- .../Components/ActorDeployedEntity.cs | 2 +- .../Components/ActorSkill.cs | 2 +- .../Components/ActorSkillCooldown.cs | 4 +- .../Components/ActorVaalSkill.cs | 2 +- .../Components/Animated.cs | 2 +- .../Components/Buffs.cs | 4 +- .../Components/Charges.cs | 2 +- .../Components/Chest.cs | 2 +- .../Components/ComponentHeader.cs | 2 +- .../Components/Life.cs | 2 +- .../Components/Mods.cs | 4 +- .../Components/Player.cs | 4 +- .../Components/Positioned.cs | 2 +- .../Components/Render.cs | 4 +- .../Components/Shrine.cs | 2 +- .../Components/StateMachine.cs | 4 +- .../Components/Stats.cs | 4 +- .../Components/Targetable.cs | 2 +- .../Components/Transitionable.cs | 2 +- .../Components/TriggerableBlockage.cs | 2 +- .../Components/WorldItem.cs | 2 +- .../Entities/Entity.cs | 4 +- .../Entities/EntityTreeNode.cs | 2 +- .../Natives/StdBucket.cs | 2 +- .../Natives/StdList.cs | 2 +- .../Natives/StdMap.cs | 2 +- .../Natives/StdTuple.cs | 2 +- .../Natives/StdVector.cs | 2 +- .../Natives/StdWString.cs | 2 +- .../Natives/Util.cs | 2 +- .../Nexus.GameOffsets.csproj} | 0 .../States/AreaInstance.cs | 4 +- .../States/AreaLoading.cs | 2 +- .../States/ImportantUiElements.cs | 2 +- .../States/InGameState.cs | 2 +- .../States/Inventory.cs | 4 +- .../States/ServerData.cs | 4 +- .../States/WorldData.cs | 2 +- .../Humanizer.cs | 4 +- .../InterceptionInputController.cs | 4 +- .../Nexus.Input.csproj} | 2 +- .../ScanCodes.cs | 2 +- .../SendInputController.cs | 4 +- .../interception.dll | Bin .../IInventoryManager.cs | 6 +- .../InventoryManager.cs | 10 +- .../InventoryTracker.cs | 6 +- src/Nexus.Inventory/Nexus.Inventory.csproj | 13 + .../StashCalibrator.cs | 8 +- .../ItemReader.cs | 6 +- .../Nexus.Items.csproj} | 4 +- .../SidekickBootstrapper.cs | 2 +- .../SidekickSettingsStub.cs | 2 +- .../Diagnostics/MemoryDiagnostics.cs | 6 +- .../Diagnostics/QuestNameLookup.cs | 2 +- .../Files/DatFile.cs | 2 +- .../Files/FileRootScanner.cs | 2 +- .../Files/FilesContainer.cs | 2 +- .../Files/IDatRowParser.cs | 2 +- .../Files/MapPinRow.cs | 2 +- .../Files/QuestRow.cs | 2 +- .../Files/QuestStateRow.cs | 2 +- .../Files/WorldAreaRow.cs | 2 +- .../GameMemoryReader.cs | 4 +- .../GameOffsets.cs | 2 +- .../GameStateReader.cs | 2 +- .../Infrastructure/ComponentReader.cs | 6 +- .../Infrastructure/MemoryContext.cs | 2 +- .../Infrastructure/MemoryProfiler.cs | 2 +- .../Infrastructure/MsvcStringReader.cs | 2 +- .../Infrastructure/Native.cs | 2 +- .../Infrastructure/ObjectRegistry.cs | 2 +- .../Infrastructure/PatternScanner.cs | 2 +- .../Infrastructure/ProcessMemory.cs | 2 +- .../Infrastructure/RttiResolver.cs | 2 +- .../Nexus.Memory.csproj} | 2 +- .../Objects/AreaInstance.cs | 4 +- .../Objects/AreaLoading.cs | 4 +- .../Objects/AreaTemplate.cs | 2 +- .../Objects/EntityList.cs | 8 +- .../Objects/GameStateType.cs | 2 +- .../Objects/GameStates.cs | 4 +- .../Objects/InGameState.cs | 6 +- .../Objects/PlayerSkills.cs | 6 +- .../Objects/QuestFlags.cs | 4 +- .../Objects/Terrain.cs | 6 +- .../Objects/UIElements.cs | 2 +- .../Objects/WorldData.cs | 6 +- .../QuestStateLookup.cs | 2 +- .../RemoteObject.cs | 2 +- .../Snapshots/ConnectedAreaInfo.cs | 2 +- .../Snapshots/Entity.cs | 2 +- .../Snapshots/GameStateSnapshot.cs | 4 +- .../Snapshots/QuestLinkedEntry.cs | 2 +- .../Snapshots/QuestSnapshot.cs | 4 +- .../Snapshots/QuestStateEntry.cs | 2 +- .../Snapshots/SkillSnapshot.cs | 4 +- .../Snapshots/UIElementNode.cs | 2 +- .../Snapshots/UiQuestEntry.cs | 2 +- .../Snapshots/WalkabilityGrid.cs | 2 +- .../AtlasPanorama.cs | 6 +- .../IconDetector.cs | 2 +- .../MinimapCapture.cs | 6 +- .../NavigationExecutor.cs | 8 +- .../NavigationTypes.cs | 4 +- .../Nexus.Navigation.csproj} | 6 +- .../PathFinder.cs | 2 +- .../PerspectiveCalibrator.cs | 6 +- .../StuckDetector.cs | 2 +- .../WallColorTracker.cs | 2 +- .../WorldMap.cs | 2 +- .../NavigationController.cs | 4 +- .../Nexus.Pathfinding.csproj} | 2 +- .../PathFinder.cs | 4 +- .../BossDetector.cs | 4 +- .../DaemonTypes.cs | 4 +- .../DesktopDuplication.cs | 4 +- .../DetectGridHandler.cs | 4 +- .../DetectionTypes.cs | 2 +- .../DiffCropHandler.cs | 4 +- .../EdgeCropHandler.cs | 4 +- .../EnemyDetector.cs | 6 +- .../FramePipeline.cs | 2 +- .../FramePipelineService.cs | 2 +- .../FrameSaver.cs | 4 +- .../GameStateDetector.cs | 4 +- .../GdiCapture.cs | 4 +- .../GridHandler.cs | 4 +- .../GridReader.cs | 4 +- .../HudReader.cs | 6 +- .../IFrameConsumer.cs | 2 +- .../IOcrEngine.cs | 2 +- .../IScreenCapture.cs | 4 +- .../IScreenReader.cs | 4 +- .../ImagePreprocessor.cs | 2 +- .../ImageUtils.cs | 2 +- .../LootDebugDetector.cs | 2 +- .../LootLabel.cs | 2 +- .../Nexus.Screen.csproj} | 2 +- .../Ocr/EasyOcrEngine.cs | 2 +- .../Ocr/OcrEngineFactory.cs | 2 +- .../Ocr/OneOcrEngine.cs | 2 +- .../Ocr/WinOcrEngine.cs | 2 +- .../OnnxYoloDetector.cs | 2 +- .../PythonDetectBridge.cs | 2 +- .../PythonOcrBridge.cs | 2 +- .../ScreenCapture.cs | 4 +- .../ScreenFrame.cs | 4 +- .../ScreenReader.cs | 6 +- .../SignalProcessing.cs | 2 +- .../TemplateMatchHandler.cs | 4 +- .../Bridge/SimInputController.cs | 149 ++++++ src/Nexus.Simulator/Bridge/SimPoller.cs | 112 ++++ src/Nexus.Simulator/Bridge/SimStateBuilder.cs | 115 +++++ src/Nexus.Simulator/Config/SimConfig.cs | 47 ++ src/Nexus.Simulator/Nexus.Simulator.csproj | 23 + src/Nexus.Simulator/Program.cs | 264 ++++++++++ src/Nexus.Simulator/Rendering/DebugPanel.cs | 131 +++++ .../Rendering/EffectRenderer.cs | 92 ++++ .../Rendering/EntityRenderer.cs | 86 ++++ src/Nexus.Simulator/Rendering/PathRenderer.cs | 37 ++ src/Nexus.Simulator/Rendering/SimRenderer.cs | 134 +++++ .../Rendering/TerrainRenderer.cs | 99 ++++ .../Rendering/VeldridImGuiRenderer.cs | 482 ++++++++++++++++++ src/Nexus.Simulator/World/SimEnemy.cs | 76 +++ src/Nexus.Simulator/World/SimPlayer.cs | 58 +++ src/Nexus.Simulator/World/SimProjectile.cs | 34 ++ src/Nexus.Simulator/World/SimSkillEffect.cs | 26 + src/Nexus.Simulator/World/SimWorld.cs | 432 ++++++++++++++++ src/Nexus.Simulator/World/TerrainGenerator.cs | 175 +++++++ .../CombatSystem.cs | 4 +- .../LootSystem.cs | 4 +- .../MovementSystem.cs | 4 +- .../NavigationSystem.cs | 6 +- .../Nexus.Systems.csproj} | 2 +- .../ResourceSystem.cs | 4 +- .../ThreatSystem.cs | 4 +- .../ITradeMonitor.cs | 4 +- .../Nexus.Trade.csproj} | 2 +- .../Selectors.cs | 2 +- .../TradeDaemonBridge.cs | 4 +- src/{Automata.Ui => Nexus.Ui}/App.axaml | 4 +- src/{Automata.Ui => Nexus.Ui}/App.axaml.cs | 32 +- .../Converters/ValueConverters.cs | 6 +- .../Nexus.Ui.csproj} | 20 +- .../Overlay/D2dNativeMethods.cs | 2 +- .../Overlay/D2dOverlay.cs | 10 +- .../Overlay/D2dRenderContext.cs | 2 +- .../Overlay/IOverlayLayer.cs | 6 +- .../Overlay/Layers/D2dDebugTextLayer.cs | 2 +- .../Overlay/Layers/D2dEnemyBoxLayer.cs | 2 +- .../Overlay/Layers/D2dEntityLabelLayer.cs | 10 +- .../Overlay/Layers/D2dHudInfoLayer.cs | 2 +- .../Overlay/Layers/D2dLootLabelLayer.cs | 2 +- src/{Automata.Ui => Nexus.Ui}/Program.cs | 4 +- .../ViewModels/AtlasViewModel.cs | 6 +- .../ViewModels/CraftingViewModel.cs | 8 +- .../ViewModels/DebugViewModel.cs | 10 +- .../ViewModels/MainWindowViewModel.cs | 10 +- .../ViewModels/MappingViewModel.cs | 8 +- .../ViewModels/MemoryViewModel.cs | 10 +- .../ViewModels/ModPoolViewModel.cs | 4 +- .../ViewModels/NexusViewModel.cs} | 20 +- .../ViewModels/ObjectBrowserViewModel.cs | 6 +- .../ViewModels/ParsedModViewModel.cs | 2 +- .../ViewModels/SettingsViewModel.cs | 8 +- .../ViewModels/SkillProfileViewModel.cs | 4 +- .../ViewModels/StashTabViewModel.cs | 4 +- .../Views/MainWindow.axaml | 16 +- .../Views/MainWindow.axaml.cs | 6 +- src/{Automata.Ui => Nexus.Ui}/app.manifest | 2 +- src/Roboto.Core/Roboto.Core.csproj | 7 - 281 files changed, 3188 insertions(+), 611 deletions(-) rename Automata.sln => Nexus.sln (75%) create mode 100644 imgui.ini delete mode 100644 src/Automata.Bot/Automata.Bot.csproj delete mode 100644 src/Automata.Inventory/Automata.Inventory.csproj rename src/{Automata.Bot => Nexus.Bot}/AtlasExecutor.cs (98%) rename src/{Automata.Bot => Nexus.Bot}/BotOrchestrator.cs (99%) rename src/{Automata.Bot => Nexus.Bot}/CombatManager.cs (98%) rename src/{Automata.Bot => Nexus.Bot}/CraftingExecutor.cs (98%) rename src/{Automata.Bot => Nexus.Bot}/DiamondExecutor.cs (98%) rename src/{Automata.Bot => Nexus.Bot}/FlaskManager.cs (96%) rename src/{Automata.Bot => Nexus.Bot}/GameExecutor.cs (98%) rename src/{Automata.Bot => Nexus.Bot}/KulemakExecutor.cs (99%) rename src/{Automata.Bot => Nexus.Bot}/MappingExecutor.cs (99%) create mode 100644 src/Nexus.Bot/Nexus.Bot.csproj rename src/{Automata.Bot => Nexus.Bot}/ScrapExecutor.cs (97%) rename src/{Automata.Bot => Nexus.Bot}/TradeExecutor.cs (97%) rename src/{Automata.Bot => Nexus.Bot}/TradeQueue.cs (97%) rename src/{Roboto.Core => Nexus.Core}/ActionQueue.cs (99%) rename src/{Roboto.Core => Nexus.Core}/Actions.cs (97%) rename src/{Roboto.Core => Nexus.Core}/BotConfig.cs (97%) rename src/{Roboto.Core => Nexus.Core}/Buff.cs (89%) rename src/{Roboto.Core => Nexus.Core}/CharacterProfile.cs (98%) rename src/{Roboto.Core => Nexus.Core}/CombatSettings.cs (93%) rename src/{Automata.Core => Nexus.Core}/ConfigStore.cs (99%) rename src/{Automata.Core => Nexus.Core}/Delays.cs (92%) rename src/{Roboto.Core => Nexus.Core}/EntitySnapshot.cs (98%) rename src/{Roboto.Core => Nexus.Core}/Enums.cs (93%) rename src/{Roboto.Core => Nexus.Core}/FlaskSettings.cs (93%) rename src/{Roboto.Core => Nexus.Core}/FlaskState.cs (91%) rename src/{Roboto.Core => Nexus.Core}/GameState.cs (98%) rename src/{Roboto.Core => Nexus.Core}/GroundEffect.cs (92%) rename src/{Automata.Core => Nexus.Core}/Helpers.cs (95%) rename src/{Roboto.Core => Nexus.Core}/IInputController.cs (95%) rename src/{Roboto.Core => Nexus.Core}/ISystem.cs (88%) rename src/{Automata.Core => Nexus.Core}/LinkManager.cs (99%) rename src/{Automata.Core => Nexus.Core}/Logging.cs (95%) rename src/{Automata.Core => Nexus.Core}/ModPoolService.cs (99%) rename src/{Automata.Core/Automata.Core.csproj => Nexus.Core/Nexus.Core.csproj} (86%) rename src/{Roboto.Core => Nexus.Core}/PlayerState.cs (98%) rename src/{Automata.Core => Nexus.Core}/Poe2ScoutClient.cs (98%) rename src/{Roboto.Core => Nexus.Core}/ProfileManager.cs (99%) rename src/{Roboto.Core => Nexus.Core}/QuestInfo.cs (96%) rename src/{Roboto.Core => Nexus.Core}/QuestProgress.cs (96%) rename src/{Automata.Core => Nexus.Core}/RePoETypes.cs (98%) rename src/{Roboto.Core => Nexus.Core}/SkillProfile.cs (99%) rename src/{Roboto.Core => Nexus.Core}/SkillState.cs (97%) rename src/{Automata.Core => Nexus.Core}/StashCalibration.cs (95%) rename src/{Roboto.Core => Nexus.Core}/TargetSelection.cs (82%) rename src/{Roboto.Core => Nexus.Core}/TerrainQuery.cs (99%) rename src/{Roboto.Core => Nexus.Core}/ThreatMap.cs (95%) rename src/{Automata.Core => Nexus.Core}/Types.cs (99%) rename src/{Roboto.Core => Nexus.Core}/UiQuestInfo.cs (93%) rename src/{Roboto.Core => Nexus.Core}/WalkabilitySnapshot.cs (93%) rename src/{Roboto.Core => Nexus.Core}/WorldToScreen.cs (97%) rename src/{Roboto.Data => Nexus.Data}/AreaGraph.cs (99%) rename src/{Roboto.Data => Nexus.Data}/AreaNameLookup.cs (98%) rename src/{Roboto.Data => Nexus.Data}/EntityClassifier.cs (98%) rename src/{Roboto.Data => Nexus.Data}/EntityMapper.cs (96%) rename src/{Roboto.Data => Nexus.Data}/GameDataCache.cs (98%) rename src/{Roboto.Data => Nexus.Data}/GameStateEnricher.cs (98%) rename src/{Roboto.Data => Nexus.Data}/MemoryPoller.cs (99%) rename src/{Roboto.Data/Roboto.Data.csproj => Nexus.Data/Nexus.Data.csproj} (70%) rename src/{Automata.Game => Nexus.Game}/ClipboardHelper.cs (99%) rename src/{Automata.Game => Nexus.Game}/GameController.cs (98%) rename src/{Automata.Game => Nexus.Game}/IGameController.cs (97%) rename src/{Automata.Game => Nexus.Game}/InputSender.cs (99%) rename src/{Automata.Game/Automata.Game.csproj => Nexus.Game/Nexus.Game.csproj} (80%) rename src/{Automata.Game => Nexus.Game}/WindowManager.cs (99%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Actor.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/ActorDeployedEntity.cs (89%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/ActorSkill.cs (96%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/ActorSkillCooldown.cs (92%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/ActorVaalSkill.cs (91%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Animated.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Buffs.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Charges.cs (93%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Chest.cs (94%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/ComponentHeader.cs (87%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Life.cs (95%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Mods.cs (91%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Player.cs (87%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Positioned.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Render.cs (93%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Shrine.cs (88%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/StateMachine.cs (86%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Stats.cs (91%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Targetable.cs (94%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/Transitionable.cs (89%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/TriggerableBlockage.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Components/WorldItem.cs (91%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Entities/Entity.cs (96%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Entities/EntityTreeNode.cs (97%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdBucket.cs (87%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdList.cs (94%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdMap.cs (96%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdTuple.cs (92%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdVector.cs (90%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/StdWString.cs (95%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/Natives/Util.cs (86%) rename src/{Roboto.GameOffsets/Roboto.GameOffsets.csproj => Nexus.GameOffsets/Nexus.GameOffsets.csproj} (100%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/AreaInstance.cs (97%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/AreaLoading.cs (94%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/ImportantUiElements.cs (95%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/InGameState.cs (96%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/Inventory.cs (95%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/ServerData.cs (91%) rename src/{Roboto.GameOffsets => Nexus.GameOffsets}/States/WorldData.cs (97%) rename src/{Roboto.Input => Nexus.Input}/Humanizer.cs (98%) rename src/{Roboto.Input => Nexus.Input}/InterceptionInputController.cs (99%) rename src/{Roboto.Input/Roboto.Input.csproj => Nexus.Input/Nexus.Input.csproj} (89%) rename src/{Roboto.Input => Nexus.Input}/ScanCodes.cs (98%) rename src/{Roboto.Input => Nexus.Input}/SendInputController.cs (99%) rename src/{Roboto.Input => Nexus.Input}/interception.dll (100%) rename src/{Automata.Inventory => Nexus.Inventory}/IInventoryManager.cs (94%) rename src/{Automata.Inventory => Nexus.Inventory}/InventoryManager.cs (99%) rename src/{Automata.Inventory => Nexus.Inventory}/InventoryTracker.cs (98%) create mode 100644 src/Nexus.Inventory/Nexus.Inventory.csproj rename src/{Automata.Inventory => Nexus.Inventory}/StashCalibrator.cs (98%) rename src/{Automata.Items => Nexus.Items}/ItemReader.cs (96%) rename src/{Automata.Items/Automata.Items.csproj => Nexus.Items/Nexus.Items.csproj} (76%) rename src/{Automata.Items => Nexus.Items}/SidekickBootstrapper.cs (98%) rename src/{Automata.Items => Nexus.Items}/SidekickSettingsStub.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Diagnostics/MemoryDiagnostics.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Diagnostics/QuestNameLookup.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Files/DatFile.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Files/FileRootScanner.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Files/FilesContainer.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Files/IDatRowParser.cs (97%) rename src/{Roboto.Memory => Nexus.Memory}/Files/MapPinRow.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Files/QuestRow.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Files/QuestStateRow.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Files/WorldAreaRow.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/GameMemoryReader.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/GameOffsets.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/GameStateReader.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/ComponentReader.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/MemoryContext.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/MemoryProfiler.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/MsvcStringReader.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/Native.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/ObjectRegistry.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/PatternScanner.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/ProcessMemory.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Infrastructure/RttiResolver.cs (98%) rename src/{Roboto.Memory/Roboto.Memory.csproj => Nexus.Memory/Nexus.Memory.csproj} (84%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/AreaInstance.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/AreaLoading.cs (94%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/AreaTemplate.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/EntityList.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/GameStateType.cs (95%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/GameStates.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/InGameState.cs (95%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/PlayerSkills.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/QuestFlags.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/Terrain.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/UIElements.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Objects/WorldData.cs (94%) rename src/{Roboto.Memory => Nexus.Memory}/QuestStateLookup.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/RemoteObject.cs (97%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/ConnectedAreaInfo.cs (94%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/Entity.cs (99%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/GameStateSnapshot.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/QuestLinkedEntry.cs (98%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/QuestSnapshot.cs (88%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/QuestStateEntry.cs (93%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/SkillSnapshot.cs (92%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/UIElementNode.cs (95%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/UiQuestEntry.cs (96%) rename src/{Roboto.Memory => Nexus.Memory}/Snapshots/WalkabilityGrid.cs (94%) rename src/{Automata.Navigation => Nexus.Navigation}/AtlasPanorama.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/IconDetector.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/MinimapCapture.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/NavigationExecutor.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/NavigationTypes.cs (99%) rename src/{Automata.Navigation/Automata.Navigation.csproj => Nexus.Navigation/Nexus.Navigation.csproj} (73%) rename src/{Automata.Navigation => Nexus.Navigation}/PathFinder.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/PerspectiveCalibrator.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/StuckDetector.cs (96%) rename src/{Automata.Navigation => Nexus.Navigation}/WallColorTracker.cs (99%) rename src/{Automata.Navigation => Nexus.Navigation}/WorldMap.cs (99%) rename src/{Roboto.Navigation => Nexus.Pathfinding}/NavigationController.cs (99%) rename src/{Roboto.Navigation/Roboto.Navigation.csproj => Nexus.Pathfinding/Nexus.Pathfinding.csproj} (83%) rename src/{Roboto.Navigation => Nexus.Pathfinding}/PathFinder.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/BossDetector.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/DaemonTypes.cs (97%) rename src/{Automata.Screen => Nexus.Screen}/DesktopDuplication.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/DetectGridHandler.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/DetectionTypes.cs (95%) rename src/{Automata.Screen => Nexus.Screen}/DiffCropHandler.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/EdgeCropHandler.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/EnemyDetector.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/FramePipeline.cs (97%) rename src/{Automata.Screen => Nexus.Screen}/FramePipelineService.cs (96%) rename src/{Automata.Screen => Nexus.Screen}/FrameSaver.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/GameStateDetector.cs (95%) rename src/{Automata.Screen => Nexus.Screen}/GdiCapture.cs (95%) rename src/{Automata.Screen => Nexus.Screen}/GridHandler.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/GridReader.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/HudReader.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/IFrameConsumer.cs (73%) rename src/{Automata.Screen => Nexus.Screen}/IOcrEngine.cs (83%) rename src/{Automata.Screen => Nexus.Screen}/IScreenCapture.cs (69%) rename src/{Automata.Screen => Nexus.Screen}/IScreenReader.cs (96%) rename src/{Automata.Screen => Nexus.Screen}/ImagePreprocessor.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/ImageUtils.cs (97%) rename src/{Automata.Screen => Nexus.Screen}/LootDebugDetector.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/LootLabel.cs (99%) rename src/{Automata.Screen/Automata.Screen.csproj => Nexus.Screen/Nexus.Screen.csproj} (91%) rename src/{Automata.Screen => Nexus.Screen}/Ocr/EasyOcrEngine.cs (96%) rename src/{Automata.Screen => Nexus.Screen}/Ocr/OcrEngineFactory.cs (93%) rename src/{Automata.Screen => Nexus.Screen}/Ocr/OneOcrEngine.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/Ocr/WinOcrEngine.cs (98%) rename src/{Automata.Screen => Nexus.Screen}/OnnxYoloDetector.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/PythonDetectBridge.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/PythonOcrBridge.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/ScreenCapture.cs (96%) rename src/{Automata.Screen => Nexus.Screen}/ScreenFrame.cs (93%) rename src/{Automata.Screen => Nexus.Screen}/ScreenReader.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/SignalProcessing.cs (99%) rename src/{Automata.Screen => Nexus.Screen}/TemplateMatchHandler.cs (98%) create mode 100644 src/Nexus.Simulator/Bridge/SimInputController.cs create mode 100644 src/Nexus.Simulator/Bridge/SimPoller.cs create mode 100644 src/Nexus.Simulator/Bridge/SimStateBuilder.cs create mode 100644 src/Nexus.Simulator/Config/SimConfig.cs create mode 100644 src/Nexus.Simulator/Nexus.Simulator.csproj create mode 100644 src/Nexus.Simulator/Program.cs create mode 100644 src/Nexus.Simulator/Rendering/DebugPanel.cs create mode 100644 src/Nexus.Simulator/Rendering/EffectRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/EntityRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/PathRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/SimRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/TerrainRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs create mode 100644 src/Nexus.Simulator/World/SimEnemy.cs create mode 100644 src/Nexus.Simulator/World/SimPlayer.cs create mode 100644 src/Nexus.Simulator/World/SimProjectile.cs create mode 100644 src/Nexus.Simulator/World/SimSkillEffect.cs create mode 100644 src/Nexus.Simulator/World/SimWorld.cs create mode 100644 src/Nexus.Simulator/World/TerrainGenerator.cs rename src/{Roboto.Systems => Nexus.Systems}/CombatSystem.cs (99%) rename src/{Roboto.Systems => Nexus.Systems}/LootSystem.cs (86%) rename src/{Roboto.Systems => Nexus.Systems}/MovementSystem.cs (97%) rename src/{Roboto.Systems => Nexus.Systems}/NavigationSystem.cs (85%) rename src/{Roboto.Systems/Roboto.Systems.csproj => Nexus.Systems/Nexus.Systems.csproj} (83%) rename src/{Roboto.Systems => Nexus.Systems}/ResourceSystem.cs (97%) rename src/{Roboto.Systems => Nexus.Systems}/ThreatSystem.cs (98%) rename src/{Automata.Trade => Nexus.Trade}/ITradeMonitor.cs (93%) rename src/{Automata.Trade/Automata.Trade.csproj => Nexus.Trade/Nexus.Trade.csproj} (77%) rename src/{Automata.Trade => Nexus.Trade}/Selectors.cs (97%) rename src/{Automata.Trade => Nexus.Trade}/TradeDaemonBridge.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/App.axaml (92%) rename src/{Automata.Ui => Nexus.Ui}/App.axaml.cs (89%) rename src/{Automata.Ui => Nexus.Ui}/Converters/ValueConverters.cs (98%) rename src/{Automata.Ui/Automata.Ui.csproj => Nexus.Ui/Nexus.Ui.csproj} (76%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/D2dNativeMethods.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/D2dOverlay.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/D2dRenderContext.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/IOverlayLayer.cs (91%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/Layers/D2dDebugTextLayer.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/Layers/D2dEnemyBoxLayer.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/Layers/D2dEntityLabelLayer.cs (94%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/Layers/D2dHudInfoLayer.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/Overlay/Layers/D2dLootLabelLayer.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/Program.cs (94%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/AtlasViewModel.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/CraftingViewModel.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/DebugViewModel.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/MainWindowViewModel.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/MappingViewModel.cs (97%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/MemoryViewModel.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/ModPoolViewModel.cs (93%) rename src/{Automata.Ui/ViewModels/RobotoViewModel.cs => Nexus.Ui/ViewModels/NexusViewModel.cs} (98%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/ObjectBrowserViewModel.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/ParsedModViewModel.cs (97%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/SettingsViewModel.cs (99%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/SkillProfileViewModel.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/ViewModels/StashTabViewModel.cs (95%) rename src/{Automata.Ui => Nexus.Ui}/Views/MainWindow.axaml (99%) rename src/{Automata.Ui => Nexus.Ui}/Views/MainWindow.axaml.cs (98%) rename src/{Automata.Ui => Nexus.Ui}/app.manifest (88%) delete mode 100644 src/Roboto.Core/Roboto.Core.csproj diff --git a/Automata.sln b/Nexus.sln similarity index 75% rename from Automata.sln rename to Nexus.sln index 26c298e..05014f4 100644 --- a/Automata.sln +++ b/Nexus.sln @@ -5,29 +5,39 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Core", "src\Automata.Core\Automata.Core.csproj", "{6432F6A5-11A0-4960-AFFC-E810D4325C35}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Core", "src\Nexus.Core\Nexus.Core.csproj", "{A31E6F94-A702-4B58-8317-83658E556B5C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Game", "src\Automata.Game\Automata.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.GameOffsets", "src\Nexus.GameOffsets\Nexus.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Screen", "src\Automata.Screen\Automata.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Memory", "src\Nexus.Memory\Nexus.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Items", "src\Automata.Items\Automata.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Input", "src\Nexus.Input\Nexus.Input.csproj", "{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Trade", "src\Automata.Trade\Automata.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Systems", "src\Nexus.Systems\Nexus.Systems.csproj", "{95AC4C34-26A0-4D7F-A712-375EB28B54B8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Log", "src\Automata.Log\Automata.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Pathfinding", "src\Nexus.Pathfinding\Nexus.Pathfinding.csproj", "{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Bot", "src\Automata.Bot\Automata.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Data", "src\Nexus.Data\Nexus.Data.csproj", "{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Inventory", "src\Automata.Inventory\Automata.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Engine", "src\Nexus.Engine\Nexus.Engine.csproj", "{C2E97306-20E4-4A69-A7AB-541A72614C76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Ui", "src\Automata.Ui\Automata.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Game", "src\Nexus.Game\Nexus.Game.csproj", "{97B8362D-777C-4ED1-B964-D6598B333E4C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\Automata.Navigation\Automata.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Screen", "src\Nexus.Screen\Nexus.Screen.csproj", "{F92C5EA2-8999-41BC-9B28-D52AD5F3542C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Memory", "src\Roboto.Memory\Roboto.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Items", "src\Nexus.Items\Nexus.Items.csproj", "{9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.GameOffsets", "src\Roboto.GameOffsets\Roboto.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Trade", "src\Nexus.Trade\Nexus.Trade.csproj", "{8F73A696-EB54-4C6F-9603-5A6BAC5D334A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Log", "src\Nexus.Log\Nexus.Log.csproj", "{B68D787D-7A83-4D8F-9F10-0B72C2E99B49}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Bot", "src\Nexus.Bot\Nexus.Bot.csproj", "{188C4F87-153F-4182-B816-9FB56F08CF3A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Inventory", "src\Nexus.Inventory\Nexus.Inventory.csproj", "{F186DDC8-6843-43E9-8BD3-9F914C5E784E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Navigation", "src\Nexus.Navigation\Nexus.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Ui", "src\Nexus.Ui\Nexus.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{652F700E-4F84-4E66-BD62-717D3A8D6FBC}" EndProject @@ -47,19 +57,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Data", "lib\Sideki EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sidekick.Data.Builder", "lib\Sidekick\src\Sidekick.Data.Builder\Sidekick.Data.Builder.csproj", "{E5C26A34-5EDF-488B-93C7-F8738F2CEB97}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "roboto", "roboto", "{D1A2B3C4-E5F6-7890-ABCD-EF1234567890}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Core", "src\Roboto.Core\Roboto.Core.csproj", "{A31E6F94-A702-4B58-8317-83658E556B5C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Input", "src\Roboto.Input\Roboto.Input.csproj", "{E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Systems", "src\Roboto.Systems\Roboto.Systems.csproj", "{95AC4C34-26A0-4D7F-A712-375EB28B54B8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Engine", "src\Roboto.Engine\Roboto.Engine.csproj", "{C2E97306-20E4-4A69-A7AB-541A72614C76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Navigation", "src\Roboto.Navigation\Roboto.Navigation.csproj", "{F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Data", "src\Roboto.Data\Roboto.Data.csproj", "{1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nexus.Simulator", "src\Nexus.Simulator\Nexus.Simulator.csproj", "{9198C826-9356-4763-87EF-BBC7166B745B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -70,10 +68,38 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {6432F6A5-11A0-4960-AFFC-E810D4325C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6432F6A5-11A0-4960-AFFC-E810D4325C35}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6432F6A5-11A0-4960-AFFC-E810D4325C35}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6432F6A5-11A0-4960-AFFC-E810D4325C35}.Release|Any CPU.Build.0 = Release|Any CPU + {A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.Build.0 = Release|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU + {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.Build.0 = Release|Any CPU + {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.Build.0 = Release|Any CPU + {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.Build.0 = Release|Any CPU + {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.Build.0 = Release|Any CPU {97B8362D-777C-4ED1-B964-D6598B333E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {97B8362D-777C-4ED1-B964-D6598B333E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU {97B8362D-777C-4ED1-B964-D6598B333E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -102,22 +128,14 @@ Global {F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Debug|Any CPU.Build.0 = Debug|Any CPU {F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Release|Any CPU.ActiveCfg = Release|Any CPU {F186DDC8-6843-43E9-8BD3-9F914C5E784E}.Release|Any CPU.Build.0 = Release|Any CPU - {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.Build.0 = Release|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU - {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU + {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.Build.0 = Release|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.Build.0 = Debug|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -150,33 +168,20 @@ Global {E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Release|Any CPU.ActiveCfg = Release|Any CPU {E5C26A34-5EDF-488B-93C7-F8738F2CEB97}.Release|Any CPU.Build.0 = Release|Any CPU - {A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A31E6F94-A702-4B58-8317-83658E556B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A31E6F94-A702-4B58-8317-83658E556B5C}.Release|Any CPU.Build.0 = Release|Any CPU - {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A}.Release|Any CPU.Build.0 = Release|Any CPU - {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {95AC4C34-26A0-4D7F-A712-375EB28B54B8}.Release|Any CPU.Build.0 = Release|Any CPU - {C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2E97306-20E4-4A69-A7AB-541A72614C76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2E97306-20E4-4A69-A7AB-541A72614C76}.Release|Any CPU.Build.0 = Release|Any CPU - {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D}.Release|Any CPU.Build.0 = Release|Any CPU - {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU + {9198C826-9356-4763-87EF-BBC7166B745B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9198C826-9356-4763-87EF-BBC7166B745B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9198C826-9356-4763-87EF-BBC7166B745B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9198C826-9356-4763-87EF-BBC7166B745B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {A31E6F94-A702-4B58-8317-83658E556B5C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {95AC4C34-26A0-4D7F-A712-375EB28B54B8} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {C2E97306-20E4-4A69-A7AB-541A72614C76} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {97B8362D-777C-4ED1-B964-D6598B333E4C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {F92C5EA2-8999-41BC-9B28-D52AD5F3542C} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {9CAB0D49-1E24-4F76-ABF8-9A5ED6819F00} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} @@ -184,10 +189,8 @@ Global {B68D787D-7A83-4D8F-9F10-0B72C2E99B49} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {188C4F87-153F-4182-B816-9FB56F08CF3A} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} - {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} + {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {B858F6F2-389F-475A-87FE-E4E01DA3E948} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {6FEA655D-18E4-4DA1-839F-A41433B03FBB} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {74FD0F88-86BC-49AE-9A16-136D92A10090} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} @@ -196,12 +199,6 @@ Global {8CEE036C-A229-4F22-BD0E-D7CDAE13E54F} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {9428D5D4-4061-467A-BD26-C1FEED95E8E6} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {E5C26A34-5EDF-488B-93C7-F8738F2CEB97} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} - {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} - {A31E6F94-A702-4B58-8317-83658E556B5C} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {E61E96C5-3DE7-4B31-A7AD-CCA99BEF8E4A} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {95AC4C34-26A0-4D7F-A712-375EB28B54B8} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {C2E97306-20E4-4A69-A7AB-541A72614C76} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {F4B5C6D7-E8F9-0A1B-2C3D-4E5F6A7B8C9D} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} - {1A2B3C4D-5E6F-7A8B-9C0D-E1F2A3B4C5D6} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} + {9198C826-9356-4763-87EF-BBC7166B745B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} EndGlobalSection EndGlobal diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 0000000..6e55fd4 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,15 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Simulator Controls] +Pos=60,60 +Size=219,425 +Collapsed=0 + +[Window][Simulator] +Pos=11,220 +Size=1200,681 +Collapsed=0 + diff --git a/src/Automata.Bot/Automata.Bot.csproj b/src/Automata.Bot/Automata.Bot.csproj deleted file mode 100644 index 77ae1f4..0000000 --- a/src/Automata.Bot/Automata.Bot.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - net8.0-windows10.0.19041.0 - enable - enable - - - - - - - - - - - - diff --git a/src/Automata.Inventory/Automata.Inventory.csproj b/src/Automata.Inventory/Automata.Inventory.csproj deleted file mode 100644 index bfb3ac5..0000000 --- a/src/Automata.Inventory/Automata.Inventory.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - net8.0-windows10.0.19041.0 - enable - enable - - - - - - - - diff --git a/src/Automata.Bot/AtlasExecutor.cs b/src/Nexus.Bot/AtlasExecutor.cs similarity index 98% rename from src/Automata.Bot/AtlasExecutor.cs rename to src/Nexus.Bot/AtlasExecutor.cs index 2f4e307..65ff5c0 100644 --- a/src/Automata.Bot/AtlasExecutor.cs +++ b/src/Nexus.Bot/AtlasExecutor.cs @@ -1,11 +1,11 @@ -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.Navigation; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.Navigation; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Captures the full endgame atlas as a panorama image. diff --git a/src/Automata.Bot/BotOrchestrator.cs b/src/Nexus.Bot/BotOrchestrator.cs similarity index 99% rename from src/Automata.Bot/BotOrchestrator.cs rename to src/Nexus.Bot/BotOrchestrator.cs index 708f005..ab2ca51 100644 --- a/src/Automata.Bot/BotOrchestrator.cs +++ b/src/Nexus.Bot/BotOrchestrator.cs @@ -1,13 +1,13 @@ -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.GameLog; -using Automata.Navigation; -using Automata.Screen; -using Automata.Trade; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.GameLog; +using Nexus.Navigation; +using Nexus.Screen; +using Nexus.Trade; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class BotStatus { diff --git a/src/Automata.Bot/CombatManager.cs b/src/Nexus.Bot/CombatManager.cs similarity index 98% rename from src/Automata.Bot/CombatManager.cs rename to src/Nexus.Bot/CombatManager.cs index d494cde..44ce7cf 100644 --- a/src/Automata.Bot/CombatManager.cs +++ b/src/Nexus.Bot/CombatManager.cs @@ -1,10 +1,10 @@ using System.Diagnostics; -using Automata.Core; -using Automata.Game; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Manages the attack state machine (click → hold) with mana monitoring and flask usage. diff --git a/src/Automata.Bot/CraftingExecutor.cs b/src/Nexus.Bot/CraftingExecutor.cs similarity index 98% rename from src/Automata.Bot/CraftingExecutor.cs rename to src/Nexus.Bot/CraftingExecutor.cs index 3ba22e0..e7add43 100644 --- a/src/Automata.Bot/CraftingExecutor.cs +++ b/src/Nexus.Bot/CraftingExecutor.cs @@ -1,9 +1,9 @@ -using Automata.Core; -using Automata.Game; -using Automata.Items; +using Nexus.Core; +using Nexus.Game; +using Nexus.Items; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class CraftingExecutor { diff --git a/src/Automata.Bot/DiamondExecutor.cs b/src/Nexus.Bot/DiamondExecutor.cs similarity index 98% rename from src/Automata.Bot/DiamondExecutor.cs rename to src/Nexus.Bot/DiamondExecutor.cs index 590a58f..fd1ab7f 100644 --- a/src/Automata.Bot/DiamondExecutor.cs +++ b/src/Nexus.Bot/DiamondExecutor.cs @@ -1,12 +1,12 @@ using System.Collections.Concurrent; -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.Screen; -using Automata.Trade; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.Screen; +using Nexus.Trade; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class DiamondExecutor { diff --git a/src/Automata.Bot/FlaskManager.cs b/src/Nexus.Bot/FlaskManager.cs similarity index 96% rename from src/Automata.Bot/FlaskManager.cs rename to src/Nexus.Bot/FlaskManager.cs index 476b4b7..591a043 100644 --- a/src/Automata.Bot/FlaskManager.cs +++ b/src/Nexus.Bot/FlaskManager.cs @@ -1,9 +1,9 @@ using System.Diagnostics; -using Automata.Game; -using Automata.Screen; +using Nexus.Game; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Monitors life/mana and presses flask keys when they drop below thresholds. diff --git a/src/Automata.Bot/GameExecutor.cs b/src/Nexus.Bot/GameExecutor.cs similarity index 98% rename from src/Automata.Bot/GameExecutor.cs rename to src/Nexus.Bot/GameExecutor.cs index 1f7386e..4f6ebe6 100644 --- a/src/Automata.Bot/GameExecutor.cs +++ b/src/Nexus.Bot/GameExecutor.cs @@ -1,11 +1,11 @@ using System.Diagnostics; -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Base class for game executors that interact with the game world. diff --git a/src/Automata.Bot/KulemakExecutor.cs b/src/Nexus.Bot/KulemakExecutor.cs similarity index 99% rename from src/Automata.Bot/KulemakExecutor.cs rename to src/Nexus.Bot/KulemakExecutor.cs index 1abd402..e97088a 100644 --- a/src/Automata.Bot/KulemakExecutor.cs +++ b/src/Nexus.Bot/KulemakExecutor.cs @@ -1,12 +1,12 @@ -using Automata.Core; -using Automata.Game; -using Automata.GameLog; -using Automata.Inventory; -using Automata.Navigation; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.GameLog; +using Nexus.Inventory; +using Nexus.Navigation; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Kulemak-specific boss run executor: scripted 4-phase + ring fight, diff --git a/src/Automata.Bot/MappingExecutor.cs b/src/Nexus.Bot/MappingExecutor.cs similarity index 99% rename from src/Automata.Bot/MappingExecutor.cs rename to src/Nexus.Bot/MappingExecutor.cs index a8979ea..88ed649 100644 --- a/src/Automata.Bot/MappingExecutor.cs +++ b/src/Nexus.Bot/MappingExecutor.cs @@ -1,13 +1,13 @@ using System.Diagnostics; -using Automata.Core; -using Automata.Game; -using Automata.GameLog; -using Automata.Inventory; -using Automata.Navigation; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.GameLog; +using Nexus.Inventory; +using Nexus.Navigation; +using Nexus.Screen; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; /// /// Shared infrastructure for any map/boss activity: combat loop, WASD navigation, diff --git a/src/Nexus.Bot/Nexus.Bot.csproj b/src/Nexus.Bot/Nexus.Bot.csproj new file mode 100644 index 0000000..d2d07a4 --- /dev/null +++ b/src/Nexus.Bot/Nexus.Bot.csproj @@ -0,0 +1,17 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + + + + + + + + + + + + diff --git a/src/Automata.Bot/ScrapExecutor.cs b/src/Nexus.Bot/ScrapExecutor.cs similarity index 97% rename from src/Automata.Bot/ScrapExecutor.cs rename to src/Nexus.Bot/ScrapExecutor.cs index 3631265..dc4f051 100644 --- a/src/Automata.Bot/ScrapExecutor.cs +++ b/src/Nexus.Bot/ScrapExecutor.cs @@ -1,11 +1,11 @@ -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.Screen; -using Automata.Trade; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.Screen; +using Nexus.Trade; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class ScrapExecutor { diff --git a/src/Automata.Bot/TradeExecutor.cs b/src/Nexus.Bot/TradeExecutor.cs similarity index 97% rename from src/Automata.Bot/TradeExecutor.cs rename to src/Nexus.Bot/TradeExecutor.cs index dfb1a34..acac3ce 100644 --- a/src/Automata.Bot/TradeExecutor.cs +++ b/src/Nexus.Bot/TradeExecutor.cs @@ -1,11 +1,11 @@ -using Automata.Core; -using Automata.Game; -using Automata.Inventory; -using Automata.Screen; -using Automata.Trade; +using Nexus.Core; +using Nexus.Game; +using Nexus.Inventory; +using Nexus.Screen; +using Nexus.Trade; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class TradeExecutor { diff --git a/src/Automata.Bot/TradeQueue.cs b/src/Nexus.Bot/TradeQueue.cs similarity index 97% rename from src/Automata.Bot/TradeQueue.cs rename to src/Nexus.Bot/TradeQueue.cs index 5521247..7ae9387 100644 --- a/src/Automata.Bot/TradeQueue.cs +++ b/src/Nexus.Bot/TradeQueue.cs @@ -1,7 +1,7 @@ -using Automata.Core; +using Nexus.Core; using Serilog; -namespace Automata.Bot; +namespace Nexus.Bot; public class TradeQueue { diff --git a/src/Roboto.Core/ActionQueue.cs b/src/Nexus.Core/ActionQueue.cs similarity index 99% rename from src/Roboto.Core/ActionQueue.cs rename to src/Nexus.Core/ActionQueue.cs index 0d3a4fd..377c87b 100644 --- a/src/Roboto.Core/ActionQueue.cs +++ b/src/Nexus.Core/ActionQueue.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class ActionQueue { diff --git a/src/Roboto.Core/Actions.cs b/src/Nexus.Core/Actions.cs similarity index 97% rename from src/Roboto.Core/Actions.cs rename to src/Nexus.Core/Actions.cs index 9bd6e4b..453c5f5 100644 --- a/src/Roboto.Core/Actions.cs +++ b/src/Nexus.Core/Actions.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public enum ClickType { Left, Right, Middle } public enum KeyActionType { Press, Down, Up } diff --git a/src/Roboto.Core/BotConfig.cs b/src/Nexus.Core/BotConfig.cs similarity index 97% rename from src/Roboto.Core/BotConfig.cs rename to src/Nexus.Core/BotConfig.cs index 230c502..e06fe25 100644 --- a/src/Roboto.Core/BotConfig.cs +++ b/src/Nexus.Core/BotConfig.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class BotConfig { diff --git a/src/Roboto.Core/Buff.cs b/src/Nexus.Core/Buff.cs similarity index 89% rename from src/Roboto.Core/Buff.cs rename to src/Nexus.Core/Buff.cs index e80ba65..85aab5a 100644 --- a/src/Roboto.Core/Buff.cs +++ b/src/Nexus.Core/Buff.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public record Buff { diff --git a/src/Roboto.Core/CharacterProfile.cs b/src/Nexus.Core/CharacterProfile.cs similarity index 98% rename from src/Roboto.Core/CharacterProfile.cs rename to src/Nexus.Core/CharacterProfile.cs index 8518c32..b592d32 100644 --- a/src/Roboto.Core/CharacterProfile.cs +++ b/src/Nexus.Core/CharacterProfile.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class CharacterProfile { diff --git a/src/Roboto.Core/CombatSettings.cs b/src/Nexus.Core/CombatSettings.cs similarity index 93% rename from src/Roboto.Core/CombatSettings.cs rename to src/Nexus.Core/CombatSettings.cs index 02cf2a9..a511432 100644 --- a/src/Roboto.Core/CombatSettings.cs +++ b/src/Nexus.Core/CombatSettings.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class CombatSettings { diff --git a/src/Automata.Core/ConfigStore.cs b/src/Nexus.Core/ConfigStore.cs similarity index 99% rename from src/Automata.Core/ConfigStore.cs rename to src/Nexus.Core/ConfigStore.cs index 6c93261..d0fef6d 100644 --- a/src/Automata.Core/ConfigStore.cs +++ b/src/Nexus.Core/ConfigStore.cs @@ -2,7 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Serilog; -namespace Automata.Core; +namespace Nexus.Core; public class SavedLink { diff --git a/src/Automata.Core/Delays.cs b/src/Nexus.Core/Delays.cs similarity index 92% rename from src/Automata.Core/Delays.cs rename to src/Nexus.Core/Delays.cs index 01b9f16..907aad0 100644 --- a/src/Automata.Core/Delays.cs +++ b/src/Nexus.Core/Delays.cs @@ -1,4 +1,4 @@ -namespace Automata.Core; +namespace Nexus.Core; public static class Delays { diff --git a/src/Roboto.Core/EntitySnapshot.cs b/src/Nexus.Core/EntitySnapshot.cs similarity index 98% rename from src/Roboto.Core/EntitySnapshot.cs rename to src/Nexus.Core/EntitySnapshot.cs index 7ede015..4e4900d 100644 --- a/src/Roboto.Core/EntitySnapshot.cs +++ b/src/Nexus.Core/EntitySnapshot.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public enum EntityCategory { diff --git a/src/Roboto.Core/Enums.cs b/src/Nexus.Core/Enums.cs similarity index 93% rename from src/Roboto.Core/Enums.cs rename to src/Nexus.Core/Enums.cs index 1f8821c..1a41bea 100644 --- a/src/Roboto.Core/Enums.cs +++ b/src/Nexus.Core/Enums.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public enum DangerLevel { diff --git a/src/Roboto.Core/FlaskSettings.cs b/src/Nexus.Core/FlaskSettings.cs similarity index 93% rename from src/Roboto.Core/FlaskSettings.cs rename to src/Nexus.Core/FlaskSettings.cs index 8b7da24..39dbf6d 100644 --- a/src/Roboto.Core/FlaskSettings.cs +++ b/src/Nexus.Core/FlaskSettings.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class FlaskSettings { diff --git a/src/Roboto.Core/FlaskState.cs b/src/Nexus.Core/FlaskState.cs similarity index 91% rename from src/Roboto.Core/FlaskState.cs rename to src/Nexus.Core/FlaskState.cs index 6a4c074..3576f0e 100644 --- a/src/Roboto.Core/FlaskState.cs +++ b/src/Nexus.Core/FlaskState.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public record FlaskState { diff --git a/src/Roboto.Core/GameState.cs b/src/Nexus.Core/GameState.cs similarity index 98% rename from src/Roboto.Core/GameState.cs rename to src/Nexus.Core/GameState.cs index 37b4fca..7baf4c7 100644 --- a/src/Roboto.Core/GameState.cs +++ b/src/Nexus.Core/GameState.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public class GameState { diff --git a/src/Roboto.Core/GroundEffect.cs b/src/Nexus.Core/GroundEffect.cs similarity index 92% rename from src/Roboto.Core/GroundEffect.cs rename to src/Nexus.Core/GroundEffect.cs index ab7f3f8..f14f752 100644 --- a/src/Roboto.Core/GroundEffect.cs +++ b/src/Nexus.Core/GroundEffect.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public record GroundEffect { diff --git a/src/Automata.Core/Helpers.cs b/src/Nexus.Core/Helpers.cs similarity index 95% rename from src/Automata.Core/Helpers.cs rename to src/Nexus.Core/Helpers.cs index 19f5205..48b3c16 100644 --- a/src/Automata.Core/Helpers.cs +++ b/src/Nexus.Core/Helpers.cs @@ -1,4 +1,4 @@ -namespace Automata.Core; +namespace Nexus.Core; public static class Helpers { diff --git a/src/Roboto.Core/IInputController.cs b/src/Nexus.Core/IInputController.cs similarity index 95% rename from src/Roboto.Core/IInputController.cs rename to src/Nexus.Core/IInputController.cs index 24f9b86..394e3c1 100644 --- a/src/Roboto.Core/IInputController.cs +++ b/src/Nexus.Core/IInputController.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public interface IInputController { diff --git a/src/Roboto.Core/ISystem.cs b/src/Nexus.Core/ISystem.cs similarity index 88% rename from src/Roboto.Core/ISystem.cs rename to src/Nexus.Core/ISystem.cs index 05b4564..e904755 100644 --- a/src/Roboto.Core/ISystem.cs +++ b/src/Nexus.Core/ISystem.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public interface ISystem { diff --git a/src/Automata.Core/LinkManager.cs b/src/Nexus.Core/LinkManager.cs similarity index 99% rename from src/Automata.Core/LinkManager.cs rename to src/Nexus.Core/LinkManager.cs index 7915a9a..32f0ec2 100644 --- a/src/Automata.Core/LinkManager.cs +++ b/src/Nexus.Core/LinkManager.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Automata.Core; +namespace Nexus.Core; public class TradeLink { diff --git a/src/Automata.Core/Logging.cs b/src/Nexus.Core/Logging.cs similarity index 95% rename from src/Automata.Core/Logging.cs rename to src/Nexus.Core/Logging.cs index bd4f173..742b887 100644 --- a/src/Automata.Core/Logging.cs +++ b/src/Nexus.Core/Logging.cs @@ -1,7 +1,7 @@ using Serilog; using Serilog.Events; -namespace Automata.Core; +namespace Nexus.Core; public static class Logging { diff --git a/src/Automata.Core/ModPoolService.cs b/src/Nexus.Core/ModPoolService.cs similarity index 99% rename from src/Automata.Core/ModPoolService.cs rename to src/Nexus.Core/ModPoolService.cs index 4aeeddc..4000c1f 100644 --- a/src/Automata.Core/ModPoolService.cs +++ b/src/Nexus.Core/ModPoolService.cs @@ -2,7 +2,7 @@ using System.Net.Http; using System.Text.Json; using Serilog; -namespace Automata.Core; +namespace Nexus.Core; public class ModPoolService { diff --git a/src/Automata.Core/Automata.Core.csproj b/src/Nexus.Core/Nexus.Core.csproj similarity index 86% rename from src/Automata.Core/Automata.Core.csproj rename to src/Nexus.Core/Nexus.Core.csproj index 5aff918..4aadc25 100644 --- a/src/Automata.Core/Automata.Core.csproj +++ b/src/Nexus.Core/Nexus.Core.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/Roboto.Core/PlayerState.cs b/src/Nexus.Core/PlayerState.cs similarity index 98% rename from src/Roboto.Core/PlayerState.cs rename to src/Nexus.Core/PlayerState.cs index 1db0729..bcd5f66 100644 --- a/src/Roboto.Core/PlayerState.cs +++ b/src/Nexus.Core/PlayerState.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public record PlayerState { diff --git a/src/Automata.Core/Poe2ScoutClient.cs b/src/Nexus.Core/Poe2ScoutClient.cs similarity index 98% rename from src/Automata.Core/Poe2ScoutClient.cs rename to src/Nexus.Core/Poe2ScoutClient.cs index 51c07ff..8422d9d 100644 --- a/src/Automata.Core/Poe2ScoutClient.cs +++ b/src/Nexus.Core/Poe2ScoutClient.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Serilog; -namespace Automata.Core; +namespace Nexus.Core; public static class Poe2ScoutClient { diff --git a/src/Roboto.Core/ProfileManager.cs b/src/Nexus.Core/ProfileManager.cs similarity index 99% rename from src/Roboto.Core/ProfileManager.cs rename to src/Nexus.Core/ProfileManager.cs index 3130296..868d2d5 100644 --- a/src/Roboto.Core/ProfileManager.cs +++ b/src/Nexus.Core/ProfileManager.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Roboto.Core; +namespace Nexus.Core; public class ProfileConfig { diff --git a/src/Roboto.Core/QuestInfo.cs b/src/Nexus.Core/QuestInfo.cs similarity index 96% rename from src/Roboto.Core/QuestInfo.cs rename to src/Nexus.Core/QuestInfo.cs index 1f75fc2..c62de53 100644 --- a/src/Roboto.Core/QuestInfo.cs +++ b/src/Nexus.Core/QuestInfo.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public class QuestInfo { diff --git a/src/Roboto.Core/QuestProgress.cs b/src/Nexus.Core/QuestProgress.cs similarity index 96% rename from src/Roboto.Core/QuestProgress.cs rename to src/Nexus.Core/QuestProgress.cs index 56a76cc..3929f78 100644 --- a/src/Roboto.Core/QuestProgress.cs +++ b/src/Nexus.Core/QuestProgress.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public record QuestProgress { diff --git a/src/Automata.Core/RePoETypes.cs b/src/Nexus.Core/RePoETypes.cs similarity index 98% rename from src/Automata.Core/RePoETypes.cs rename to src/Nexus.Core/RePoETypes.cs index d62f665..9034de7 100644 --- a/src/Automata.Core/RePoETypes.cs +++ b/src/Nexus.Core/RePoETypes.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Automata.Core; +namespace Nexus.Core; public class RePoEMod { diff --git a/src/Roboto.Core/SkillProfile.cs b/src/Nexus.Core/SkillProfile.cs similarity index 99% rename from src/Roboto.Core/SkillProfile.cs rename to src/Nexus.Core/SkillProfile.cs index 0c6a890..481d0d3 100644 --- a/src/Roboto.Core/SkillProfile.cs +++ b/src/Nexus.Core/SkillProfile.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public enum SkillInputType { KeyPress, LeftClick, RightClick, MiddleClick } diff --git a/src/Roboto.Core/SkillState.cs b/src/Nexus.Core/SkillState.cs similarity index 97% rename from src/Roboto.Core/SkillState.cs rename to src/Nexus.Core/SkillState.cs index 6168749..2205d36 100644 --- a/src/Roboto.Core/SkillState.cs +++ b/src/Nexus.Core/SkillState.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public record SkillState { diff --git a/src/Automata.Core/StashCalibration.cs b/src/Nexus.Core/StashCalibration.cs similarity index 95% rename from src/Automata.Core/StashCalibration.cs rename to src/Nexus.Core/StashCalibration.cs index 587b8b6..98f1cda 100644 --- a/src/Automata.Core/StashCalibration.cs +++ b/src/Nexus.Core/StashCalibration.cs @@ -1,4 +1,4 @@ -namespace Automata.Core; +namespace Nexus.Core; public class StashTabInfo { diff --git a/src/Roboto.Core/TargetSelection.cs b/src/Nexus.Core/TargetSelection.cs similarity index 82% rename from src/Roboto.Core/TargetSelection.cs rename to src/Nexus.Core/TargetSelection.cs index fbcc872..02d3309 100644 --- a/src/Roboto.Core/TargetSelection.cs +++ b/src/Nexus.Core/TargetSelection.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public enum TargetSelection { diff --git a/src/Roboto.Core/TerrainQuery.cs b/src/Nexus.Core/TerrainQuery.cs similarity index 99% rename from src/Roboto.Core/TerrainQuery.cs rename to src/Nexus.Core/TerrainQuery.cs index df403b6..87ed1da 100644 --- a/src/Roboto.Core/TerrainQuery.cs +++ b/src/Nexus.Core/TerrainQuery.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; /// /// Terrain line-of-sight and walkable direction queries on the walkability grid. diff --git a/src/Roboto.Core/ThreatMap.cs b/src/Nexus.Core/ThreatMap.cs similarity index 95% rename from src/Roboto.Core/ThreatMap.cs rename to src/Nexus.Core/ThreatMap.cs index 79e0214..bfeac13 100644 --- a/src/Roboto.Core/ThreatMap.cs +++ b/src/Nexus.Core/ThreatMap.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public class ThreatMap { diff --git a/src/Automata.Core/Types.cs b/src/Nexus.Core/Types.cs similarity index 99% rename from src/Automata.Core/Types.cs rename to src/Nexus.Core/Types.cs index 69a4deb..972159c 100644 --- a/src/Automata.Core/Types.cs +++ b/src/Nexus.Core/Types.cs @@ -1,4 +1,4 @@ -namespace Automata.Core; +namespace Nexus.Core; public record Region(int X, int Y, int Width, int Height); diff --git a/src/Roboto.Core/UiQuestInfo.cs b/src/Nexus.Core/UiQuestInfo.cs similarity index 93% rename from src/Roboto.Core/UiQuestInfo.cs rename to src/Nexus.Core/UiQuestInfo.cs index e120874..6c0aa1c 100644 --- a/src/Roboto.Core/UiQuestInfo.cs +++ b/src/Nexus.Core/UiQuestInfo.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; /// /// Active quest info as displayed in the game UI. diff --git a/src/Roboto.Core/WalkabilitySnapshot.cs b/src/Nexus.Core/WalkabilitySnapshot.cs similarity index 93% rename from src/Roboto.Core/WalkabilitySnapshot.cs rename to src/Nexus.Core/WalkabilitySnapshot.cs index bf4dfb0..cb1fecc 100644 --- a/src/Roboto.Core/WalkabilitySnapshot.cs +++ b/src/Nexus.Core/WalkabilitySnapshot.cs @@ -1,4 +1,4 @@ -namespace Roboto.Core; +namespace Nexus.Core; public record WalkabilitySnapshot { diff --git a/src/Roboto.Core/WorldToScreen.cs b/src/Nexus.Core/WorldToScreen.cs similarity index 97% rename from src/Roboto.Core/WorldToScreen.cs rename to src/Nexus.Core/WorldToScreen.cs index 33f12d0..96e5a76 100644 --- a/src/Roboto.Core/WorldToScreen.cs +++ b/src/Nexus.Core/WorldToScreen.cs @@ -1,6 +1,6 @@ using System.Numerics; -namespace Roboto.Core; +namespace Nexus.Core; public static class WorldToScreen { diff --git a/src/Roboto.Data/AreaGraph.cs b/src/Nexus.Data/AreaGraph.cs similarity index 99% rename from src/Roboto.Data/AreaGraph.cs rename to src/Nexus.Data/AreaGraph.cs index 4e5aaf4..c7d57f5 100644 --- a/src/Roboto.Data/AreaGraph.cs +++ b/src/Nexus.Data/AreaGraph.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Roboto.Data; +namespace Nexus.Data; public record AreaNode( string Id, diff --git a/src/Roboto.Data/AreaNameLookup.cs b/src/Nexus.Data/AreaNameLookup.cs similarity index 98% rename from src/Roboto.Data/AreaNameLookup.cs rename to src/Nexus.Data/AreaNameLookup.cs index 7e7ca68..34bb3ae 100644 --- a/src/Roboto.Data/AreaNameLookup.cs +++ b/src/Nexus.Data/AreaNameLookup.cs @@ -1,6 +1,6 @@ using System.Text.Json; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Resolves area IDs (e.g. "G1_4") to display names (e.g. "The Grelwood") diff --git a/src/Roboto.Data/EntityClassifier.cs b/src/Nexus.Data/EntityClassifier.cs similarity index 98% rename from src/Roboto.Data/EntityClassifier.cs rename to src/Nexus.Data/EntityClassifier.cs index c390fed..cebf3e5 100644 --- a/src/Roboto.Data/EntityClassifier.cs +++ b/src/Nexus.Data/EntityClassifier.cs @@ -1,6 +1,6 @@ -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Classifies entities from path + component data into EntityCategory. diff --git a/src/Roboto.Data/EntityMapper.cs b/src/Nexus.Data/EntityMapper.cs similarity index 96% rename from src/Roboto.Data/EntityMapper.cs rename to src/Nexus.Data/EntityMapper.cs index d1edf12..6234a1e 100644 --- a/src/Roboto.Data/EntityMapper.cs +++ b/src/Nexus.Data/EntityMapper.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; -using MemEntity = Roboto.Memory.Entity; +using Nexus.Core; +using MemEntity = Nexus.Memory.Entity; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Maps raw Memory.Entity → Core.EntitySnapshot. Single source of truth for entity mapping. diff --git a/src/Roboto.Data/GameDataCache.cs b/src/Nexus.Data/GameDataCache.cs similarity index 98% rename from src/Roboto.Data/GameDataCache.cs rename to src/Nexus.Data/GameDataCache.cs index 0ca85c0..5859878 100644 --- a/src/Roboto.Data/GameDataCache.cs +++ b/src/Nexus.Data/GameDataCache.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; -using Roboto.Memory; +using Nexus.Core; +using Nexus.Memory; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Immutable snapshot of player position for lock-free cross-thread reads. diff --git a/src/Roboto.Data/GameStateEnricher.cs b/src/Nexus.Data/GameStateEnricher.cs similarity index 98% rename from src/Roboto.Data/GameStateEnricher.cs rename to src/Nexus.Data/GameStateEnricher.cs index 3112e60..1cb35fb 100644 --- a/src/Roboto.Data/GameStateEnricher.cs +++ b/src/Nexus.Data/GameStateEnricher.cs @@ -1,7 +1,7 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Computes all derived fields on GameState once per tick. diff --git a/src/Roboto.Data/MemoryPoller.cs b/src/Nexus.Data/MemoryPoller.cs similarity index 99% rename from src/Roboto.Data/MemoryPoller.cs rename to src/Nexus.Data/MemoryPoller.cs index ef46ffe..8fda25e 100644 --- a/src/Roboto.Data/MemoryPoller.cs +++ b/src/Nexus.Data/MemoryPoller.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using System.Numerics; -using Roboto.Memory; -using Roboto.Core; +using Nexus.Memory; +using Nexus.Core; using Serilog; -namespace Roboto.Data; +namespace Nexus.Data; /// /// Owns the memory read thread. Runs a two-tier loop: @@ -28,7 +28,7 @@ public sealed class MemoryPoller : IDisposable private nint _playerLifeAddr; private nint _inGameStateAddr; private nint _controllerAddr; - private Roboto.Memory.GameOffsets? _offsets; + private Nexus.Memory.GameOffsets? _offsets; private ProcessMemory? _mem; private int _hotHz; @@ -64,7 +64,7 @@ public sealed class MemoryPoller : IDisposable _thread = new Thread(PollLoop) { - Name = "Roboto.MemoryPoller", + Name = "Nexus.MemoryPoller", IsBackground = true, }; _thread.Start(); diff --git a/src/Roboto.Data/Roboto.Data.csproj b/src/Nexus.Data/Nexus.Data.csproj similarity index 70% rename from src/Roboto.Data/Roboto.Data.csproj rename to src/Nexus.Data/Nexus.Data.csproj index 38208e6..0d85deb 100644 --- a/src/Roboto.Data/Roboto.Data.csproj +++ b/src/Nexus.Data/Nexus.Data.csproj @@ -8,7 +8,7 @@ - - + + diff --git a/src/Automata.Game/ClipboardHelper.cs b/src/Nexus.Game/ClipboardHelper.cs similarity index 99% rename from src/Automata.Game/ClipboardHelper.cs rename to src/Nexus.Game/ClipboardHelper.cs index c895916..dc2e3c4 100644 --- a/src/Automata.Game/ClipboardHelper.cs +++ b/src/Nexus.Game/ClipboardHelper.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace Automata.Game; +namespace Nexus.Game; /// /// Win32 clipboard access without WinForms dependency. diff --git a/src/Automata.Game/GameController.cs b/src/Nexus.Game/GameController.cs similarity index 98% rename from src/Automata.Game/GameController.cs rename to src/Nexus.Game/GameController.cs index 5b82b3c..abbc59c 100644 --- a/src/Automata.Game/GameController.cs +++ b/src/Nexus.Game/GameController.cs @@ -1,7 +1,7 @@ -using Automata.Core; +using Nexus.Core; using Serilog; -namespace Automata.Game; +namespace Nexus.Game; public class GameController : IGameController { diff --git a/src/Automata.Game/IGameController.cs b/src/Nexus.Game/IGameController.cs similarity index 97% rename from src/Automata.Game/IGameController.cs rename to src/Nexus.Game/IGameController.cs index 9359f16..5b3984d 100644 --- a/src/Automata.Game/IGameController.cs +++ b/src/Nexus.Game/IGameController.cs @@ -1,4 +1,4 @@ -namespace Automata.Game; +namespace Nexus.Game; public interface IGameController { diff --git a/src/Automata.Game/InputSender.cs b/src/Nexus.Game/InputSender.cs similarity index 99% rename from src/Automata.Game/InputSender.cs rename to src/Nexus.Game/InputSender.cs index 4942aeb..b2c6c22 100644 --- a/src/Automata.Game/InputSender.cs +++ b/src/Nexus.Game/InputSender.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Automata.Core; +using Nexus.Core; -namespace Automata.Game; +namespace Nexus.Game; public class InputSender { diff --git a/src/Automata.Game/Automata.Game.csproj b/src/Nexus.Game/Nexus.Game.csproj similarity index 80% rename from src/Automata.Game/Automata.Game.csproj rename to src/Nexus.Game/Nexus.Game.csproj index fa49371..2a268b4 100644 --- a/src/Automata.Game/Automata.Game.csproj +++ b/src/Nexus.Game/Nexus.Game.csproj @@ -6,6 +6,6 @@ true - + diff --git a/src/Automata.Game/WindowManager.cs b/src/Nexus.Game/WindowManager.cs similarity index 99% rename from src/Automata.Game/WindowManager.cs rename to src/Nexus.Game/WindowManager.cs index f593cfb..168252d 100644 --- a/src/Automata.Game/WindowManager.cs +++ b/src/Nexus.Game/WindowManager.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using Serilog; -namespace Automata.Game; +namespace Nexus.Game; public class WindowManager { diff --git a/src/Roboto.GameOffsets/Components/Actor.cs b/src/Nexus.GameOffsets/Components/Actor.cs similarity index 90% rename from src/Roboto.GameOffsets/Components/Actor.cs rename to src/Nexus.GameOffsets/Components/Actor.cs index e95c6cb..a933bcd 100644 --- a/src/Roboto.GameOffsets/Components/Actor.cs +++ b/src/Nexus.GameOffsets/Components/Actor.cs @@ -1,4 +1,4 @@ -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// /// Actor component offsets — confirmed from ExileCore2. diff --git a/src/Roboto.GameOffsets/Components/ActorDeployedEntity.cs b/src/Nexus.GameOffsets/Components/ActorDeployedEntity.cs similarity index 89% rename from src/Roboto.GameOffsets/Components/ActorDeployedEntity.cs rename to src/Nexus.GameOffsets/Components/ActorDeployedEntity.cs index cfd3811..648cf78 100644 --- a/src/Roboto.GameOffsets/Components/ActorDeployedEntity.cs +++ b/src/Nexus.GameOffsets/Components/ActorDeployedEntity.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// A deployed entity (totem, mine, etc.). [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Components/ActorSkill.cs b/src/Nexus.GameOffsets/Components/ActorSkill.cs similarity index 96% rename from src/Roboto.GameOffsets/Components/ActorSkill.cs rename to src/Nexus.GameOffsets/Components/ActorSkill.cs index 5000be5..1c27db5 100644 --- a/src/Roboto.GameOffsets/Components/ActorSkill.cs +++ b/src/Nexus.GameOffsets/Components/ActorSkill.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// /// An entry in the ActiveSkills vector: shared_ptr pair (0x10 bytes). diff --git a/src/Roboto.GameOffsets/Components/ActorSkillCooldown.cs b/src/Nexus.GameOffsets/Components/ActorSkillCooldown.cs similarity index 92% rename from src/Roboto.GameOffsets/Components/ActorSkillCooldown.cs rename to src/Nexus.GameOffsets/Components/ActorSkillCooldown.cs index d0f242c..819224f 100644 --- a/src/Roboto.GameOffsets/Components/ActorSkillCooldown.cs +++ b/src/Nexus.GameOffsets/Components/ActorSkillCooldown.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// /// Cooldown state for a skill. Entries in Actor+0xB18 vector. diff --git a/src/Roboto.GameOffsets/Components/ActorVaalSkill.cs b/src/Nexus.GameOffsets/Components/ActorVaalSkill.cs similarity index 91% rename from src/Roboto.GameOffsets/Components/ActorVaalSkill.cs rename to src/Nexus.GameOffsets/Components/ActorVaalSkill.cs index 3cdd359..93c08fc 100644 --- a/src/Roboto.GameOffsets/Components/ActorVaalSkill.cs +++ b/src/Nexus.GameOffsets/Components/ActorVaalSkill.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Vaal soul tracking. [StructLayout(LayoutKind.Explicit, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Components/Animated.cs b/src/Nexus.GameOffsets/Components/Animated.cs similarity index 90% rename from src/Roboto.GameOffsets/Components/Animated.cs rename to src/Nexus.GameOffsets/Components/Animated.cs index 3766d1a..2425ff4 100644 --- a/src/Roboto.GameOffsets/Components/Animated.cs +++ b/src/Nexus.GameOffsets/Components/Animated.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Animated component — reference to the animated entity. [StructLayout(LayoutKind.Explicit, Size = 0x300)] diff --git a/src/Roboto.GameOffsets/Components/Buffs.cs b/src/Nexus.GameOffsets/Components/Buffs.cs similarity index 90% rename from src/Roboto.GameOffsets/Components/Buffs.cs rename to src/Nexus.GameOffsets/Components/Buffs.cs index 761392d..db335f9 100644 --- a/src/Roboto.GameOffsets/Components/Buffs.cs +++ b/src/Nexus.GameOffsets/Components/Buffs.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Buffs component — active status effects. [StructLayout(LayoutKind.Explicit, Size = 0x178)] diff --git a/src/Roboto.GameOffsets/Components/Charges.cs b/src/Nexus.GameOffsets/Components/Charges.cs similarity index 93% rename from src/Roboto.GameOffsets/Components/Charges.cs rename to src/Nexus.GameOffsets/Components/Charges.cs index c2231b2..4f84081 100644 --- a/src/Roboto.GameOffsets/Components/Charges.cs +++ b/src/Nexus.GameOffsets/Components/Charges.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Charges component — flask/skill charges. [StructLayout(LayoutKind.Explicit, Size = 0x20)] diff --git a/src/Roboto.GameOffsets/Components/Chest.cs b/src/Nexus.GameOffsets/Components/Chest.cs similarity index 94% rename from src/Roboto.GameOffsets/Components/Chest.cs rename to src/Nexus.GameOffsets/Components/Chest.cs index b86d397..688fffa 100644 --- a/src/Roboto.GameOffsets/Components/Chest.cs +++ b/src/Nexus.GameOffsets/Components/Chest.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Chest component. [StructLayout(LayoutKind.Explicit, Size = 0x170)] diff --git a/src/Roboto.GameOffsets/Components/ComponentHeader.cs b/src/Nexus.GameOffsets/Components/ComponentHeader.cs similarity index 87% rename from src/Roboto.GameOffsets/Components/ComponentHeader.cs rename to src/Nexus.GameOffsets/Components/ComponentHeader.cs index f0f6910..2db11b0 100644 --- a/src/Roboto.GameOffsets/Components/ComponentHeader.cs +++ b/src/Nexus.GameOffsets/Components/ComponentHeader.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Common header at the start of every component. [StructLayout(LayoutKind.Explicit, Size = 0x10)] diff --git a/src/Roboto.GameOffsets/Components/Life.cs b/src/Nexus.GameOffsets/Components/Life.cs similarity index 95% rename from src/Roboto.GameOffsets/Components/Life.cs rename to src/Nexus.GameOffsets/Components/Life.cs index f85275b..2b4e4f1 100644 --- a/src/Roboto.GameOffsets/Components/Life.cs +++ b/src/Nexus.GameOffsets/Components/Life.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Life component — contains Health, Mana, and ES vitals. [StructLayout(LayoutKind.Explicit, Size = 0x268)] diff --git a/src/Roboto.GameOffsets/Components/Mods.cs b/src/Nexus.GameOffsets/Components/Mods.cs similarity index 91% rename from src/Roboto.GameOffsets/Components/Mods.cs rename to src/Nexus.GameOffsets/Components/Mods.cs index 9184a09..20e901b 100644 --- a/src/Roboto.GameOffsets/Components/Mods.cs +++ b/src/Nexus.GameOffsets/Components/Mods.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Mods component — ModsAndObjectMagicProperties inline at +0x00. Rarity at +0x94. [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1A0)] diff --git a/src/Roboto.GameOffsets/Components/Player.cs b/src/Nexus.GameOffsets/Components/Player.cs similarity index 87% rename from src/Roboto.GameOffsets/Components/Player.cs rename to src/Nexus.GameOffsets/Components/Player.cs index 7fa7af5..74c0e35 100644 --- a/src/Roboto.GameOffsets/Components/Player.cs +++ b/src/Nexus.GameOffsets/Components/Player.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Player component — name, XP, level. [StructLayout(LayoutKind.Explicit, Size = 0x208)] diff --git a/src/Roboto.GameOffsets/Components/Positioned.cs b/src/Nexus.GameOffsets/Components/Positioned.cs similarity index 90% rename from src/Roboto.GameOffsets/Components/Positioned.cs rename to src/Nexus.GameOffsets/Components/Positioned.cs index be35bda..21da95a 100644 --- a/src/Roboto.GameOffsets/Components/Positioned.cs +++ b/src/Nexus.GameOffsets/Components/Positioned.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Positioned component — reaction (friendly/hostile/neutral). [StructLayout(LayoutKind.Explicit, Size = 0x1E8)] diff --git a/src/Roboto.GameOffsets/Components/Render.cs b/src/Nexus.GameOffsets/Components/Render.cs similarity index 93% rename from src/Roboto.GameOffsets/Components/Render.cs rename to src/Nexus.GameOffsets/Components/Render.cs index 7301451..c7f868d 100644 --- a/src/Roboto.GameOffsets/Components/Render.cs +++ b/src/Nexus.GameOffsets/Components/Render.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Render component — world position, bounds, terrain height. [StructLayout(LayoutKind.Explicit, Size = 0x1B0)] diff --git a/src/Roboto.GameOffsets/Components/Shrine.cs b/src/Nexus.GameOffsets/Components/Shrine.cs similarity index 88% rename from src/Roboto.GameOffsets/Components/Shrine.cs rename to src/Nexus.GameOffsets/Components/Shrine.cs index 680efe4..45968ab 100644 --- a/src/Roboto.GameOffsets/Components/Shrine.cs +++ b/src/Nexus.GameOffsets/Components/Shrine.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Shrine component. [StructLayout(LayoutKind.Explicit, Size = 0x28)] diff --git a/src/Roboto.GameOffsets/Components/StateMachine.cs b/src/Nexus.GameOffsets/Components/StateMachine.cs similarity index 86% rename from src/Roboto.GameOffsets/Components/StateMachine.cs rename to src/Nexus.GameOffsets/Components/StateMachine.cs index d912bf7..2ce904b 100644 --- a/src/Roboto.GameOffsets/Components/StateMachine.cs +++ b/src/Nexus.GameOffsets/Components/StateMachine.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// StateMachine component — entity state management. [StructLayout(LayoutKind.Explicit, Size = 0x178)] diff --git a/src/Roboto.GameOffsets/Components/Stats.cs b/src/Nexus.GameOffsets/Components/Stats.cs similarity index 91% rename from src/Roboto.GameOffsets/Components/Stats.cs rename to src/Nexus.GameOffsets/Components/Stats.cs index 5f74b43..c6b43db 100644 --- a/src/Roboto.GameOffsets/Components/Stats.cs +++ b/src/Nexus.GameOffsets/Components/Stats.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Stats component — item stats, weapon index, shapeshift. [StructLayout(LayoutKind.Explicit, Size = 0x180)] diff --git a/src/Roboto.GameOffsets/Components/Targetable.cs b/src/Nexus.GameOffsets/Components/Targetable.cs similarity index 94% rename from src/Roboto.GameOffsets/Components/Targetable.cs rename to src/Nexus.GameOffsets/Components/Targetable.cs index bc74585..3dfdb4e 100644 --- a/src/Roboto.GameOffsets/Components/Targetable.cs +++ b/src/Nexus.GameOffsets/Components/Targetable.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Targetable component — whether entity can be targeted/highlighted. [StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x58)] diff --git a/src/Roboto.GameOffsets/Components/Transitionable.cs b/src/Nexus.GameOffsets/Components/Transitionable.cs similarity index 89% rename from src/Roboto.GameOffsets/Components/Transitionable.cs rename to src/Nexus.GameOffsets/Components/Transitionable.cs index 25ec8d9..79f4f24 100644 --- a/src/Roboto.GameOffsets/Components/Transitionable.cs +++ b/src/Nexus.GameOffsets/Components/Transitionable.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// Transitionable component — area transition state. [StructLayout(LayoutKind.Explicit, Size = 0x128)] diff --git a/src/Roboto.GameOffsets/Components/TriggerableBlockage.cs b/src/Nexus.GameOffsets/Components/TriggerableBlockage.cs similarity index 90% rename from src/Roboto.GameOffsets/Components/TriggerableBlockage.cs rename to src/Nexus.GameOffsets/Components/TriggerableBlockage.cs index 4268448..90d1b82 100644 --- a/src/Roboto.GameOffsets/Components/TriggerableBlockage.cs +++ b/src/Nexus.GameOffsets/Components/TriggerableBlockage.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// TriggerableBlockage component — door/gate blocked state. [StructLayout(LayoutKind.Explicit, Size = 0x38)] diff --git a/src/Roboto.GameOffsets/Components/WorldItem.cs b/src/Nexus.GameOffsets/Components/WorldItem.cs similarity index 91% rename from src/Roboto.GameOffsets/Components/WorldItem.cs rename to src/Nexus.GameOffsets/Components/WorldItem.cs index ed39760..cf462e8 100644 --- a/src/Roboto.GameOffsets/Components/WorldItem.cs +++ b/src/Nexus.GameOffsets/Components/WorldItem.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Components; +namespace Nexus.GameOffsets.Components; /// WorldItem component — contains pointer to inner item entity at +0x28 (TBD from diagnostic). [StructLayout(LayoutKind.Explicit, Size = 0x30)] diff --git a/src/Roboto.GameOffsets/Entities/Entity.cs b/src/Nexus.GameOffsets/Entities/Entity.cs similarity index 96% rename from src/Roboto.GameOffsets/Entities/Entity.cs rename to src/Nexus.GameOffsets/Entities/Entity.cs index c6b7a00..d2ebfa5 100644 --- a/src/Roboto.GameOffsets/Entities/Entity.cs +++ b/src/Nexus.GameOffsets/Entities/Entity.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.Entities; +namespace Nexus.GameOffsets.Entities; /// Item struct — wraps an entity pointer for inventory items. [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Entities/EntityTreeNode.cs b/src/Nexus.GameOffsets/Entities/EntityTreeNode.cs similarity index 97% rename from src/Roboto.GameOffsets/Entities/EntityTreeNode.cs rename to src/Nexus.GameOffsets/Entities/EntityTreeNode.cs index 9b9e0cd..a37cd6e 100644 --- a/src/Roboto.GameOffsets/Entities/EntityTreeNode.cs +++ b/src/Nexus.GameOffsets/Entities/EntityTreeNode.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Entities; +namespace Nexus.GameOffsets.Entities; /// MSVC std::map tree node for entity storage. Red-black tree node layout. [StructLayout(LayoutKind.Explicit, Size = 0x30)] diff --git a/src/Roboto.GameOffsets/Natives/StdBucket.cs b/src/Nexus.GameOffsets/Natives/StdBucket.cs similarity index 87% rename from src/Roboto.GameOffsets/Natives/StdBucket.cs rename to src/Nexus.GameOffsets/Natives/StdBucket.cs index 94f2b84..ee515de 100644 --- a/src/Roboto.GameOffsets/Natives/StdBucket.cs +++ b/src/Nexus.GameOffsets/Natives/StdBucket.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// Bucket structure used in hash containers. [StructLayout(LayoutKind.Explicit, Size = 0x20)] diff --git a/src/Roboto.GameOffsets/Natives/StdList.cs b/src/Nexus.GameOffsets/Natives/StdList.cs similarity index 94% rename from src/Roboto.GameOffsets/Natives/StdList.cs rename to src/Nexus.GameOffsets/Natives/StdList.cs index a1284c7..97dc432 100644 --- a/src/Roboto.GameOffsets/Natives/StdList.cs +++ b/src/Nexus.GameOffsets/Natives/StdList.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// MSVC std::list layout: head node pointer + size. [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Natives/StdMap.cs b/src/Nexus.GameOffsets/Natives/StdMap.cs similarity index 96% rename from src/Roboto.GameOffsets/Natives/StdMap.cs rename to src/Nexus.GameOffsets/Natives/StdMap.cs index 6195ad1..fac819a 100644 --- a/src/Roboto.GameOffsets/Natives/StdMap.cs +++ b/src/Nexus.GameOffsets/Natives/StdMap.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// MSVC std::map layout: head node pointer + size. [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Natives/StdTuple.cs b/src/Nexus.GameOffsets/Natives/StdTuple.cs similarity index 92% rename from src/Roboto.GameOffsets/Natives/StdTuple.cs rename to src/Nexus.GameOffsets/Natives/StdTuple.cs index 2258e00..6d55f8a 100644 --- a/src/Roboto.GameOffsets/Natives/StdTuple.cs +++ b/src/Nexus.GameOffsets/Natives/StdTuple.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// 2D tuple (e.g., terrain dimensions). [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Natives/StdVector.cs b/src/Nexus.GameOffsets/Natives/StdVector.cs similarity index 90% rename from src/Roboto.GameOffsets/Natives/StdVector.cs rename to src/Nexus.GameOffsets/Natives/StdVector.cs index beeb591..3e3028f 100644 --- a/src/Roboto.GameOffsets/Natives/StdVector.cs +++ b/src/Nexus.GameOffsets/Natives/StdVector.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// MSVC std::vector layout: begin/end/capacity pointers. [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Roboto.GameOffsets/Natives/StdWString.cs b/src/Nexus.GameOffsets/Natives/StdWString.cs similarity index 95% rename from src/Roboto.GameOffsets/Natives/StdWString.cs rename to src/Nexus.GameOffsets/Natives/StdWString.cs index 0587dfb..cc38300 100644 --- a/src/Roboto.GameOffsets/Natives/StdWString.cs +++ b/src/Nexus.GameOffsets/Natives/StdWString.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; /// MSVC std::wstring (basic_string<wchar_t>) with SSO. [StructLayout(LayoutKind.Explicit, Size = 0x20)] diff --git a/src/Roboto.GameOffsets/Natives/Util.cs b/src/Nexus.GameOffsets/Natives/Util.cs similarity index 86% rename from src/Roboto.GameOffsets/Natives/Util.cs rename to src/Nexus.GameOffsets/Natives/Util.cs index e4972ca..2cd77ef 100644 --- a/src/Roboto.GameOffsets/Natives/Util.cs +++ b/src/Nexus.GameOffsets/Natives/Util.cs @@ -1,4 +1,4 @@ -namespace Roboto.GameOffsets.Natives; +namespace Nexus.GameOffsets.Natives; public static class Util { diff --git a/src/Roboto.GameOffsets/Roboto.GameOffsets.csproj b/src/Nexus.GameOffsets/Nexus.GameOffsets.csproj similarity index 100% rename from src/Roboto.GameOffsets/Roboto.GameOffsets.csproj rename to src/Nexus.GameOffsets/Nexus.GameOffsets.csproj diff --git a/src/Roboto.GameOffsets/States/AreaInstance.cs b/src/Nexus.GameOffsets/States/AreaInstance.cs similarity index 97% rename from src/Roboto.GameOffsets/States/AreaInstance.cs rename to src/Nexus.GameOffsets/States/AreaInstance.cs index 6e02ac2..7c6afe4 100644 --- a/src/Roboto.GameOffsets/States/AreaInstance.cs +++ b/src/Nexus.GameOffsets/States/AreaInstance.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// AreaInstance (IngameData) — current area data, entities, terrain. [StructLayout(LayoutKind.Explicit, Size = 0xCD0)] diff --git a/src/Roboto.GameOffsets/States/AreaLoading.cs b/src/Nexus.GameOffsets/States/AreaLoading.cs similarity index 94% rename from src/Roboto.GameOffsets/States/AreaLoading.cs rename to src/Nexus.GameOffsets/States/AreaLoading.cs index 6774c68..af34573 100644 --- a/src/Roboto.GameOffsets/States/AreaLoading.cs +++ b/src/Nexus.GameOffsets/States/AreaLoading.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// AreaLoading state — loading screen info. [StructLayout(LayoutKind.Explicit, Size = 0xE58)] diff --git a/src/Roboto.GameOffsets/States/ImportantUiElements.cs b/src/Nexus.GameOffsets/States/ImportantUiElements.cs similarity index 95% rename from src/Roboto.GameOffsets/States/ImportantUiElements.cs rename to src/Nexus.GameOffsets/States/ImportantUiElements.cs index b636ed3..ff5e83e 100644 --- a/src/Roboto.GameOffsets/States/ImportantUiElements.cs +++ b/src/Nexus.GameOffsets/States/ImportantUiElements.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// Important UI element pointers within InGameState. [StructLayout(LayoutKind.Explicit, Size = 0x740)] diff --git a/src/Roboto.GameOffsets/States/InGameState.cs b/src/Nexus.GameOffsets/States/InGameState.cs similarity index 96% rename from src/Roboto.GameOffsets/States/InGameState.cs rename to src/Nexus.GameOffsets/States/InGameState.cs index ff49244..f3a657f 100644 --- a/src/Roboto.GameOffsets/States/InGameState.cs +++ b/src/Nexus.GameOffsets/States/InGameState.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// InGameState — the main in-game state containing all sub-structures. [StructLayout(LayoutKind.Explicit, Size = 0x310)] diff --git a/src/Roboto.GameOffsets/States/Inventory.cs b/src/Nexus.GameOffsets/States/Inventory.cs similarity index 95% rename from src/Roboto.GameOffsets/States/Inventory.cs rename to src/Nexus.GameOffsets/States/Inventory.cs index 81d0acc..b0bb0a1 100644 --- a/src/Roboto.GameOffsets/States/Inventory.cs +++ b/src/Nexus.GameOffsets/States/Inventory.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// Pre-inventory wrapper — contains the actual inventory pointer. [StructLayout(LayoutKind.Explicit, Size = 0x10)] diff --git a/src/Roboto.GameOffsets/States/ServerData.cs b/src/Nexus.GameOffsets/States/ServerData.cs similarity index 91% rename from src/Roboto.GameOffsets/States/ServerData.cs rename to src/Nexus.GameOffsets/States/ServerData.cs index de1e35c..705d811 100644 --- a/src/Roboto.GameOffsets/States/ServerData.cs +++ b/src/Nexus.GameOffsets/States/ServerData.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Natives; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// ServerData — player server-side data. [StructLayout(LayoutKind.Explicit, Size = 0x58)] diff --git a/src/Roboto.GameOffsets/States/WorldData.cs b/src/Nexus.GameOffsets/States/WorldData.cs similarity index 97% rename from src/Roboto.GameOffsets/States/WorldData.cs rename to src/Nexus.GameOffsets/States/WorldData.cs index badd798..80a131b 100644 --- a/src/Roboto.GameOffsets/States/WorldData.cs +++ b/src/Nexus.GameOffsets/States/WorldData.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using System.Numerics; -namespace Roboto.GameOffsets.States; +namespace Nexus.GameOffsets.States; /// WorldData — world area details and camera. [StructLayout(LayoutKind.Explicit, Size = 0xA8)] diff --git a/src/Roboto.Input/Humanizer.cs b/src/Nexus.Input/Humanizer.cs similarity index 98% rename from src/Roboto.Input/Humanizer.cs rename to src/Nexus.Input/Humanizer.cs index 36a56ab..668b73d 100644 --- a/src/Roboto.Input/Humanizer.cs +++ b/src/Nexus.Input/Humanizer.cs @@ -1,6 +1,6 @@ -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Input; +namespace Nexus.Input; public sealed class Humanizer { diff --git a/src/Roboto.Input/InterceptionInputController.cs b/src/Nexus.Input/InterceptionInputController.cs similarity index 99% rename from src/Roboto.Input/InterceptionInputController.cs rename to src/Nexus.Input/InterceptionInputController.cs index e4e1ff1..12dfcae 100644 --- a/src/Roboto.Input/InterceptionInputController.cs +++ b/src/Nexus.Input/InterceptionInputController.cs @@ -1,9 +1,9 @@ using System.Runtime.InteropServices; using InputInterceptorNS; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Input; +namespace Nexus.Input; public sealed partial class InterceptionInputController : IInputController, IDisposable { diff --git a/src/Roboto.Input/Roboto.Input.csproj b/src/Nexus.Input/Nexus.Input.csproj similarity index 89% rename from src/Roboto.Input/Roboto.Input.csproj rename to src/Nexus.Input/Nexus.Input.csproj index d216727..a8e735e 100644 --- a/src/Roboto.Input/Roboto.Input.csproj +++ b/src/Nexus.Input/Nexus.Input.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Roboto.Input/ScanCodes.cs b/src/Nexus.Input/ScanCodes.cs similarity index 98% rename from src/Roboto.Input/ScanCodes.cs rename to src/Nexus.Input/ScanCodes.cs index d0432ba..959b878 100644 --- a/src/Roboto.Input/ScanCodes.cs +++ b/src/Nexus.Input/ScanCodes.cs @@ -1,4 +1,4 @@ -namespace Roboto.Input; +namespace Nexus.Input; /// /// Hardware scan codes for keyboard input via Interception driver. diff --git a/src/Roboto.Input/SendInputController.cs b/src/Nexus.Input/SendInputController.cs similarity index 99% rename from src/Roboto.Input/SendInputController.cs rename to src/Nexus.Input/SendInputController.cs index 242cb31..8e4ed9d 100644 --- a/src/Roboto.Input/SendInputController.cs +++ b/src/Nexus.Input/SendInputController.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Input; +namespace Nexus.Input; /// /// Fallback input controller using Win32 SendInput with KEYEVENTF_SCANCODE. diff --git a/src/Roboto.Input/interception.dll b/src/Nexus.Input/interception.dll similarity index 100% rename from src/Roboto.Input/interception.dll rename to src/Nexus.Input/interception.dll diff --git a/src/Automata.Inventory/IInventoryManager.cs b/src/Nexus.Inventory/IInventoryManager.cs similarity index 94% rename from src/Automata.Inventory/IInventoryManager.cs rename to src/Nexus.Inventory/IInventoryManager.cs index ee1a93d..9817755 100644 --- a/src/Automata.Inventory/IInventoryManager.cs +++ b/src/Nexus.Inventory/IInventoryManager.cs @@ -1,7 +1,7 @@ -using Automata.Core; -using Automata.Screen; +using Nexus.Core; +using Nexus.Screen; -namespace Automata.Inventory; +namespace Nexus.Inventory; public interface IInventoryManager { diff --git a/src/Automata.Inventory/InventoryManager.cs b/src/Nexus.Inventory/InventoryManager.cs similarity index 99% rename from src/Automata.Inventory/InventoryManager.cs rename to src/Nexus.Inventory/InventoryManager.cs index 1e31c55..c68dc31 100644 --- a/src/Automata.Inventory/InventoryManager.cs +++ b/src/Nexus.Inventory/InventoryManager.cs @@ -1,10 +1,10 @@ -using Automata.Core; -using Automata.Game; -using Automata.GameLog; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.GameLog; +using Nexus.Screen; using Serilog; -namespace Automata.Inventory; +namespace Nexus.Inventory; public class InventoryManager : IInventoryManager { diff --git a/src/Automata.Inventory/InventoryTracker.cs b/src/Nexus.Inventory/InventoryTracker.cs similarity index 98% rename from src/Automata.Inventory/InventoryTracker.cs rename to src/Nexus.Inventory/InventoryTracker.cs index ef33f4f..562fc63 100644 --- a/src/Automata.Inventory/InventoryTracker.cs +++ b/src/Nexus.Inventory/InventoryTracker.cs @@ -1,8 +1,8 @@ -using Automata.Core; -using Automata.Screen; +using Nexus.Core; +using Nexus.Screen; using Serilog; -namespace Automata.Inventory; +namespace Nexus.Inventory; public class PlacedItem { diff --git a/src/Nexus.Inventory/Nexus.Inventory.csproj b/src/Nexus.Inventory/Nexus.Inventory.csproj new file mode 100644 index 0000000..6d2333c --- /dev/null +++ b/src/Nexus.Inventory/Nexus.Inventory.csproj @@ -0,0 +1,13 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + + + + + + + + diff --git a/src/Automata.Inventory/StashCalibrator.cs b/src/Nexus.Inventory/StashCalibrator.cs similarity index 98% rename from src/Automata.Inventory/StashCalibrator.cs rename to src/Nexus.Inventory/StashCalibrator.cs index 657f9d6..8c2e576 100644 --- a/src/Automata.Inventory/StashCalibrator.cs +++ b/src/Nexus.Inventory/StashCalibrator.cs @@ -1,9 +1,9 @@ -using Automata.Core; -using Automata.Game; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.Screen; using Serilog; -namespace Automata.Inventory; +namespace Nexus.Inventory; public class StashCalibrator { diff --git a/src/Automata.Items/ItemReader.cs b/src/Nexus.Items/ItemReader.cs similarity index 96% rename from src/Automata.Items/ItemReader.cs rename to src/Nexus.Items/ItemReader.cs index 7475780..06b9c37 100644 --- a/src/Automata.Items/ItemReader.cs +++ b/src/Nexus.Items/ItemReader.cs @@ -1,10 +1,10 @@ -using Automata.Core; -using Automata.Game; +using Nexus.Core; +using Nexus.Game; using Sidekick.Apis.Poe.Items; using Sidekick.Apis.Poe.Trade.Parser; using Serilog; -namespace Automata.Items; +namespace Nexus.Items; /// /// Reads item data by hovering and pressing Ctrl+C to copy item text to clipboard. diff --git a/src/Automata.Items/Automata.Items.csproj b/src/Nexus.Items/Nexus.Items.csproj similarity index 76% rename from src/Automata.Items/Automata.Items.csproj rename to src/Nexus.Items/Nexus.Items.csproj index 014b17a..69f3196 100644 --- a/src/Automata.Items/Automata.Items.csproj +++ b/src/Nexus.Items/Nexus.Items.csproj @@ -5,8 +5,8 @@ enable - - + + diff --git a/src/Automata.Items/SidekickBootstrapper.cs b/src/Nexus.Items/SidekickBootstrapper.cs similarity index 98% rename from src/Automata.Items/SidekickBootstrapper.cs rename to src/Nexus.Items/SidekickBootstrapper.cs index 8b3b215..7cf81a5 100644 --- a/src/Automata.Items/SidekickBootstrapper.cs +++ b/src/Nexus.Items/SidekickBootstrapper.cs @@ -9,7 +9,7 @@ using Sidekick.Common.Settings; using Sidekick.Data; using Serilog; -namespace Automata.Items; +namespace Nexus.Items; /// /// Bootstraps a minimal Sidekick DI container for item parsing. diff --git a/src/Automata.Items/SidekickSettingsStub.cs b/src/Nexus.Items/SidekickSettingsStub.cs similarity index 98% rename from src/Automata.Items/SidekickSettingsStub.cs rename to src/Nexus.Items/SidekickSettingsStub.cs index 4a8a54b..9dfa7fe 100644 --- a/src/Automata.Items/SidekickSettingsStub.cs +++ b/src/Nexus.Items/SidekickSettingsStub.cs @@ -1,6 +1,6 @@ using Sidekick.Common.Settings; -namespace Automata.Items; +namespace Nexus.Items; /// /// Minimal ISettingsService returning defaults for Sidekick initialization. diff --git a/src/Roboto.Memory/Diagnostics/MemoryDiagnostics.cs b/src/Nexus.Memory/Diagnostics/MemoryDiagnostics.cs similarity index 99% rename from src/Roboto.Memory/Diagnostics/MemoryDiagnostics.cs rename to src/Nexus.Memory/Diagnostics/MemoryDiagnostics.cs index 95c9df4..41e9204 100644 --- a/src/Roboto.Memory/Diagnostics/MemoryDiagnostics.cs +++ b/src/Nexus.Memory/Diagnostics/MemoryDiagnostics.cs @@ -3,10 +3,10 @@ using System.Drawing.Imaging; using System.Globalization; using System.Runtime.InteropServices; using System.Text; -using Roboto.Memory.Objects; +using Nexus.Memory.Objects; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Diagnostic and scan methods extracted from GameMemoryReader. @@ -7583,7 +7583,7 @@ public sealed class MemoryDiagnostics var modsPtr = _ctx.Memory.ReadPointer(iFirst + modsIdx * 8); if (modsPtr != 0) { - var mods = _ctx.Memory.Read(modsPtr); + var mods = _ctx.Memory.Read(modsPtr); sb.AppendLine($" Mods.Rarity: {mods.Rarity}"); } } diff --git a/src/Roboto.Memory/Diagnostics/QuestNameLookup.cs b/src/Nexus.Memory/Diagnostics/QuestNameLookup.cs similarity index 98% rename from src/Roboto.Memory/Diagnostics/QuestNameLookup.cs rename to src/Nexus.Memory/Diagnostics/QuestNameLookup.cs index 3cda3e9..cae14b4 100644 --- a/src/Roboto.Memory/Diagnostics/QuestNameLookup.cs +++ b/src/Nexus.Memory/Diagnostics/QuestNameLookup.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Loads quest name mappings from a JSON file (generated by tools/dump_quest_names.py). diff --git a/src/Roboto.Memory/Files/DatFile.cs b/src/Nexus.Memory/Files/DatFile.cs similarity index 99% rename from src/Roboto.Memory/Files/DatFile.cs rename to src/Nexus.Memory/Files/DatFile.cs index a3fde7c..e220a22 100644 --- a/src/Roboto.Memory/Files/DatFile.cs +++ b/src/Nexus.Memory/Files/DatFile.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Generic wrapper for a single .dat table. Bulk-reads all rows in one RPM call, diff --git a/src/Roboto.Memory/Files/FileRootScanner.cs b/src/Nexus.Memory/Files/FileRootScanner.cs similarity index 99% rename from src/Roboto.Memory/Files/FileRootScanner.cs rename to src/Nexus.Memory/Files/FileRootScanner.cs index 7287154..12beda1 100644 --- a/src/Roboto.Memory/Files/FileRootScanner.cs +++ b/src/Nexus.Memory/Files/FileRootScanner.cs @@ -1,7 +1,7 @@ using System.Text; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Info about a discovered .dat file in memory: first record address, row size, and row count. diff --git a/src/Roboto.Memory/Files/FilesContainer.cs b/src/Nexus.Memory/Files/FilesContainer.cs similarity index 99% rename from src/Roboto.Memory/Files/FilesContainer.cs rename to src/Nexus.Memory/Files/FilesContainer.cs index f94957c..07c37c9 100644 --- a/src/Roboto.Memory/Files/FilesContainer.cs +++ b/src/Nexus.Memory/Files/FilesContainer.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Facade for all in-memory .dat file access. Owns the diff --git a/src/Roboto.Memory/Files/IDatRowParser.cs b/src/Nexus.Memory/Files/IDatRowParser.cs similarity index 97% rename from src/Roboto.Memory/Files/IDatRowParser.cs rename to src/Nexus.Memory/Files/IDatRowParser.cs index e72cbea..be9a8fb 100644 --- a/src/Roboto.Memory/Files/IDatRowParser.cs +++ b/src/Nexus.Memory/Files/IDatRowParser.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Stateless parser for a single .dat row schema. One implementation per .dat file type. diff --git a/src/Roboto.Memory/Files/MapPinRow.cs b/src/Nexus.Memory/Files/MapPinRow.cs similarity index 98% rename from src/Roboto.Memory/Files/MapPinRow.cs rename to src/Nexus.Memory/Files/MapPinRow.cs index 995c7a2..92e44e3 100644 --- a/src/Roboto.Memory/Files/MapPinRow.cs +++ b/src/Nexus.Memory/Files/MapPinRow.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Parsed row from MapPins.dat. diff --git a/src/Roboto.Memory/Files/QuestRow.cs b/src/Nexus.Memory/Files/QuestRow.cs similarity index 98% rename from src/Roboto.Memory/Files/QuestRow.cs rename to src/Nexus.Memory/Files/QuestRow.cs index 321e9b4..7d907f2 100644 --- a/src/Roboto.Memory/Files/QuestRow.cs +++ b/src/Nexus.Memory/Files/QuestRow.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Parsed row from Quest.dat. Fields mirror the in-memory Quest object layout. diff --git a/src/Roboto.Memory/Files/QuestStateRow.cs b/src/Nexus.Memory/Files/QuestStateRow.cs similarity index 99% rename from src/Roboto.Memory/Files/QuestStateRow.cs rename to src/Nexus.Memory/Files/QuestStateRow.cs index 555b725..991d816 100644 --- a/src/Roboto.Memory/Files/QuestStateRow.cs +++ b/src/Nexus.Memory/Files/QuestStateRow.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Parsed row from QuestStates.dat (208 bytes per row). diff --git a/src/Roboto.Memory/Files/WorldAreaRow.cs b/src/Nexus.Memory/Files/WorldAreaRow.cs similarity index 99% rename from src/Roboto.Memory/Files/WorldAreaRow.cs rename to src/Nexus.Memory/Files/WorldAreaRow.cs index ef95113..cb47a02 100644 --- a/src/Roboto.Memory/Files/WorldAreaRow.cs +++ b/src/Nexus.Memory/Files/WorldAreaRow.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Parsed row from WorldAreas.dat (same layout as AreaTemplate offsets). diff --git a/src/Roboto.Memory/GameMemoryReader.cs b/src/Nexus.Memory/GameMemoryReader.cs similarity index 99% rename from src/Roboto.Memory/GameMemoryReader.cs rename to src/Nexus.Memory/GameMemoryReader.cs index acf3743..9fb6667 100644 --- a/src/Roboto.Memory/GameMemoryReader.cs +++ b/src/Nexus.Memory/GameMemoryReader.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Memory.Objects; +using Nexus.Memory.Objects; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; public class GameMemoryReader : IDisposable { diff --git a/src/Roboto.Memory/GameOffsets.cs b/src/Nexus.Memory/GameOffsets.cs similarity index 99% rename from src/Roboto.Memory/GameOffsets.cs rename to src/Nexus.Memory/GameOffsets.cs index 283783d..ac0828c 100644 --- a/src/Roboto.Memory/GameOffsets.cs +++ b/src/Nexus.Memory/GameOffsets.cs @@ -3,7 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Reads/writes int as hex strings ("0x1A8") or plain numbers (424). diff --git a/src/Roboto.Memory/GameStateReader.cs b/src/Nexus.Memory/GameStateReader.cs similarity index 99% rename from src/Roboto.Memory/GameStateReader.cs rename to src/Nexus.Memory/GameStateReader.cs index 71ab564..a2d536a 100644 --- a/src/Roboto.Memory/GameStateReader.cs +++ b/src/Nexus.Memory/GameStateReader.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state. diff --git a/src/Roboto.Memory/Infrastructure/ComponentReader.cs b/src/Nexus.Memory/Infrastructure/ComponentReader.cs similarity index 99% rename from src/Roboto.Memory/Infrastructure/ComponentReader.cs rename to src/Nexus.Memory/Infrastructure/ComponentReader.cs index 2da856d..eea5517 100644 --- a/src/Roboto.Memory/Infrastructure/ComponentReader.cs +++ b/src/Nexus.Memory/Infrastructure/ComponentReader.cs @@ -1,9 +1,9 @@ using System.Text; -using Roboto.GameOffsets.Components; -using Roboto.GameOffsets.Natives; +using Nexus.GameOffsets.Components; +using Nexus.GameOffsets.Natives; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Reads entity components via ECS: component list discovery, vitals, position, component lookup. diff --git a/src/Roboto.Memory/Infrastructure/MemoryContext.cs b/src/Nexus.Memory/Infrastructure/MemoryContext.cs similarity index 98% rename from src/Roboto.Memory/Infrastructure/MemoryContext.cs rename to src/Nexus.Memory/Infrastructure/MemoryContext.cs index dc42940..9baee9e 100644 --- a/src/Roboto.Memory/Infrastructure/MemoryContext.cs +++ b/src/Nexus.Memory/Infrastructure/MemoryContext.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Shared state for all memory reader classes. Holds the process handle, offsets, registry, diff --git a/src/Roboto.Memory/Infrastructure/MemoryProfiler.cs b/src/Nexus.Memory/Infrastructure/MemoryProfiler.cs similarity index 98% rename from src/Roboto.Memory/Infrastructure/MemoryProfiler.cs rename to src/Nexus.Memory/Infrastructure/MemoryProfiler.cs index e7d5626..390a8e8 100644 --- a/src/Roboto.Memory/Infrastructure/MemoryProfiler.cs +++ b/src/Nexus.Memory/Infrastructure/MemoryProfiler.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Thread-static section profiler for memory reads. When enabled, attributes each diff --git a/src/Roboto.Memory/Infrastructure/MsvcStringReader.cs b/src/Nexus.Memory/Infrastructure/MsvcStringReader.cs similarity index 99% rename from src/Roboto.Memory/Infrastructure/MsvcStringReader.cs rename to src/Nexus.Memory/Infrastructure/MsvcStringReader.cs index c1b0014..110dcf0 100644 --- a/src/Roboto.Memory/Infrastructure/MsvcStringReader.cs +++ b/src/Nexus.Memory/Infrastructure/MsvcStringReader.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Reads MSVC std::string and std::wstring from process memory. diff --git a/src/Roboto.Memory/Infrastructure/Native.cs b/src/Nexus.Memory/Infrastructure/Native.cs similarity index 98% rename from src/Roboto.Memory/Infrastructure/Native.cs rename to src/Nexus.Memory/Infrastructure/Native.cs index 1294226..105c02e 100644 --- a/src/Roboto.Memory/Infrastructure/Native.cs +++ b/src/Nexus.Memory/Infrastructure/Native.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Roboto.Memory; +namespace Nexus.Memory; internal static partial class Native { diff --git a/src/Roboto.Memory/Infrastructure/ObjectRegistry.cs b/src/Nexus.Memory/Infrastructure/ObjectRegistry.cs similarity index 99% rename from src/Roboto.Memory/Infrastructure/ObjectRegistry.cs rename to src/Nexus.Memory/Infrastructure/ObjectRegistry.cs index 020ed1c..92f8cb5 100644 --- a/src/Roboto.Memory/Infrastructure/ObjectRegistry.cs +++ b/src/Nexus.Memory/Infrastructure/ObjectRegistry.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Persistent registry of discovered strings, organized by category. diff --git a/src/Roboto.Memory/Infrastructure/PatternScanner.cs b/src/Nexus.Memory/Infrastructure/PatternScanner.cs similarity index 99% rename from src/Roboto.Memory/Infrastructure/PatternScanner.cs rename to src/Nexus.Memory/Infrastructure/PatternScanner.cs index 379d880..ce3e9c7 100644 --- a/src/Roboto.Memory/Infrastructure/PatternScanner.cs +++ b/src/Nexus.Memory/Infrastructure/PatternScanner.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; public sealed class PatternScanner { diff --git a/src/Roboto.Memory/Infrastructure/ProcessMemory.cs b/src/Nexus.Memory/Infrastructure/ProcessMemory.cs similarity index 99% rename from src/Roboto.Memory/Infrastructure/ProcessMemory.cs rename to src/Nexus.Memory/Infrastructure/ProcessMemory.cs index 1f1d1e4..72a9435 100644 --- a/src/Roboto.Memory/Infrastructure/ProcessMemory.cs +++ b/src/Nexus.Memory/Infrastructure/ProcessMemory.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; public sealed class ProcessMemory : IDisposable { diff --git a/src/Roboto.Memory/Infrastructure/RttiResolver.cs b/src/Nexus.Memory/Infrastructure/RttiResolver.cs similarity index 98% rename from src/Roboto.Memory/Infrastructure/RttiResolver.cs rename to src/Nexus.Memory/Infrastructure/RttiResolver.cs index 0337b4f..a1b02fc 100644 --- a/src/Roboto.Memory/Infrastructure/RttiResolver.cs +++ b/src/Nexus.Memory/Infrastructure/RttiResolver.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Resolves MSVC x64 RTTI type names from vtable addresses and classifies pointers. diff --git a/src/Roboto.Memory/Roboto.Memory.csproj b/src/Nexus.Memory/Nexus.Memory.csproj similarity index 84% rename from src/Roboto.Memory/Roboto.Memory.csproj rename to src/Nexus.Memory/Nexus.Memory.csproj index ebdf0c2..d43d983 100644 --- a/src/Roboto.Memory/Roboto.Memory.csproj +++ b/src/Nexus.Memory/Nexus.Memory.csproj @@ -10,6 +10,6 @@ - + diff --git a/src/Roboto.Memory/Objects/AreaInstance.cs b/src/Nexus.Memory/Objects/AreaInstance.cs similarity index 99% rename from src/Roboto.Memory/Objects/AreaInstance.cs rename to src/Nexus.Memory/Objects/AreaInstance.cs index 644e605..0c7fb53 100644 --- a/src/Roboto.Memory/Objects/AreaInstance.cs +++ b/src/Nexus.Memory/Objects/AreaInstance.cs @@ -1,6 +1,6 @@ -using Roboto.Memory; +using Nexus.Memory; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads fields from the AreaInstance (IngameData) address. diff --git a/src/Roboto.Memory/Objects/AreaLoading.cs b/src/Nexus.Memory/Objects/AreaLoading.cs similarity index 94% rename from src/Roboto.Memory/Objects/AreaLoading.cs rename to src/Nexus.Memory/Objects/AreaLoading.cs index 37f93b2..de10f14 100644 --- a/src/Roboto.Memory/Objects/AreaLoading.cs +++ b/src/Nexus.Memory/Objects/AreaLoading.cs @@ -1,6 +1,6 @@ -using Roboto.Memory; +using Nexus.Memory; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads AreaLoading state (slot 0). Individual field reads — the full struct is 3672B, wasteful to bulk-read. diff --git a/src/Roboto.Memory/Objects/AreaTemplate.cs b/src/Nexus.Memory/Objects/AreaTemplate.cs similarity index 98% rename from src/Roboto.Memory/Objects/AreaTemplate.cs rename to src/Nexus.Memory/Objects/AreaTemplate.cs index 2ee258d..7337936 100644 --- a/src/Roboto.Memory/Objects/AreaTemplate.cs +++ b/src/Nexus.Memory/Objects/AreaTemplate.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads AreaTemplate fields from WorldData → WorldAreaDetailsPtr. diff --git a/src/Roboto.Memory/Objects/EntityList.cs b/src/Nexus.Memory/Objects/EntityList.cs similarity index 99% rename from src/Roboto.Memory/Objects/EntityList.cs rename to src/Nexus.Memory/Objects/EntityList.cs index c7dc3f4..bfd8f98 100644 --- a/src/Roboto.Memory/Objects/EntityList.cs +++ b/src/Nexus.Memory/Objects/EntityList.cs @@ -1,8 +1,8 @@ -using Roboto.Memory; -using Roboto.GameOffsets.Components; -using Roboto.GameOffsets.Entities; +using Nexus.Memory; +using Nexus.GameOffsets.Components; +using Nexus.GameOffsets.Entities; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads entity list from AreaInstance's std::map red-black tree. diff --git a/src/Roboto.Memory/Objects/GameStateType.cs b/src/Nexus.Memory/Objects/GameStateType.cs similarity index 95% rename from src/Roboto.Memory/Objects/GameStateType.cs rename to src/Nexus.Memory/Objects/GameStateType.cs index 421b04f..254083c 100644 --- a/src/Roboto.Memory/Objects/GameStateType.cs +++ b/src/Nexus.Memory/Objects/GameStateType.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Game state types by slot index. Order must match the state array in the controller. diff --git a/src/Roboto.Memory/Objects/GameStates.cs b/src/Nexus.Memory/Objects/GameStates.cs similarity index 99% rename from src/Roboto.Memory/Objects/GameStates.cs rename to src/Nexus.Memory/Objects/GameStates.cs index 1c35d2c..1089229 100644 --- a/src/Roboto.Memory/Objects/GameStates.cs +++ b/src/Nexus.Memory/Objects/GameStates.cs @@ -1,6 +1,6 @@ -using Roboto.Memory; +using Nexus.Memory; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Root state orchestrator. Reads controller from GameStateBase, resolves state slot pointers, diff --git a/src/Roboto.Memory/Objects/InGameState.cs b/src/Nexus.Memory/Objects/InGameState.cs similarity index 95% rename from src/Roboto.Memory/Objects/InGameState.cs rename to src/Nexus.Memory/Objects/InGameState.cs index 87e678c..080b1b3 100644 --- a/src/Roboto.Memory/Objects/InGameState.cs +++ b/src/Nexus.Memory/Objects/InGameState.cs @@ -1,7 +1,7 @@ -using Roboto.Memory; -using IgsStruct = Roboto.GameOffsets.States.InGameState; +using Nexus.Memory; +using IgsStruct = Nexus.GameOffsets.States.InGameState; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads InGameState struct (784B, 1 RPM instead of 4 individual reads). diff --git a/src/Roboto.Memory/Objects/PlayerSkills.cs b/src/Nexus.Memory/Objects/PlayerSkills.cs similarity index 98% rename from src/Roboto.Memory/Objects/PlayerSkills.cs rename to src/Nexus.Memory/Objects/PlayerSkills.cs index ba1dff8..68e11aa 100644 --- a/src/Roboto.Memory/Objects/PlayerSkills.cs +++ b/src/Nexus.Memory/Objects/PlayerSkills.cs @@ -1,7 +1,7 @@ -using Roboto.Memory; -using Roboto.GameOffsets.Components; +using Nexus.Memory; +using Nexus.GameOffsets.Components; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads active skills from the local player's Actor component. diff --git a/src/Roboto.Memory/Objects/QuestFlags.cs b/src/Nexus.Memory/Objects/QuestFlags.cs similarity index 99% rename from src/Roboto.Memory/Objects/QuestFlags.cs rename to src/Nexus.Memory/Objects/QuestFlags.cs index 969ee14..dc15645 100644 --- a/src/Roboto.Memory/Objects/QuestFlags.cs +++ b/src/Nexus.Memory/Objects/QuestFlags.cs @@ -1,7 +1,7 @@ -using Roboto.Memory; +using Nexus.Memory; using Serilog; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads quest flags from ServerData → PlayerServerData → QuestFlags. diff --git a/src/Roboto.Memory/Objects/Terrain.cs b/src/Nexus.Memory/Objects/Terrain.cs similarity index 98% rename from src/Roboto.Memory/Objects/Terrain.cs rename to src/Nexus.Memory/Objects/Terrain.cs index c9fe865..8c18033 100644 --- a/src/Roboto.Memory/Objects/Terrain.cs +++ b/src/Nexus.Memory/Objects/Terrain.cs @@ -1,8 +1,8 @@ -using Roboto.Memory; +using Nexus.Memory; using Serilog; -using TerrainStruct = Roboto.GameOffsets.States.Terrain; +using TerrainStruct = Nexus.GameOffsets.States.Terrain; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads terrain walkability grid from AreaInstance. diff --git a/src/Roboto.Memory/Objects/UIElements.cs b/src/Nexus.Memory/Objects/UIElements.cs similarity index 99% rename from src/Roboto.Memory/Objects/UIElements.cs rename to src/Nexus.Memory/Objects/UIElements.cs index 1bcf01b..d0672a7 100644 --- a/src/Roboto.Memory/Objects/UIElements.cs +++ b/src/Nexus.Memory/Objects/UIElements.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads the UIElement tree from InGameState → UiRootStruct → GameUi. diff --git a/src/Roboto.Memory/Objects/WorldData.cs b/src/Nexus.Memory/Objects/WorldData.cs similarity index 94% rename from src/Roboto.Memory/Objects/WorldData.cs rename to src/Nexus.Memory/Objects/WorldData.cs index 3f4ad12..e04ca65 100644 --- a/src/Roboto.Memory/Objects/WorldData.cs +++ b/src/Nexus.Memory/Objects/WorldData.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Memory; -using WdStruct = Roboto.GameOffsets.States.WorldData; +using Nexus.Memory; +using WdStruct = Nexus.GameOffsets.States.WorldData; -namespace Roboto.Memory.Objects; +namespace Nexus.Memory.Objects; /// /// Reads WorldData struct (168B, 1 RPM) and resolves the camera matrix. diff --git a/src/Roboto.Memory/QuestStateLookup.cs b/src/Nexus.Memory/QuestStateLookup.cs similarity index 99% rename from src/Roboto.Memory/QuestStateLookup.cs rename to src/Nexus.Memory/QuestStateLookup.cs index aa67409..3b8beed 100644 --- a/src/Roboto.Memory/QuestStateLookup.cs +++ b/src/Nexus.Memory/QuestStateLookup.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Builds a (questDatRowPtr, stateId) → text lookup from QuestStates.dat. diff --git a/src/Roboto.Memory/RemoteObject.cs b/src/Nexus.Memory/RemoteObject.cs similarity index 97% rename from src/Roboto.Memory/RemoteObject.cs rename to src/Nexus.Memory/RemoteObject.cs index 6afc901..cdffe06 100644 --- a/src/Roboto.Memory/RemoteObject.cs +++ b/src/Nexus.Memory/RemoteObject.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Base class for state objects that read a section of game memory. diff --git a/src/Roboto.Memory/Snapshots/ConnectedAreaInfo.cs b/src/Nexus.Memory/Snapshots/ConnectedAreaInfo.cs similarity index 94% rename from src/Roboto.Memory/Snapshots/ConnectedAreaInfo.cs rename to src/Nexus.Memory/Snapshots/ConnectedAreaInfo.cs index 02edeb3..7e752ea 100644 --- a/src/Roboto.Memory/Snapshots/ConnectedAreaInfo.cs +++ b/src/Nexus.Memory/Snapshots/ConnectedAreaInfo.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Lightweight connected area info from WorldAreas.dat for the snapshot. diff --git a/src/Roboto.Memory/Snapshots/Entity.cs b/src/Nexus.Memory/Snapshots/Entity.cs similarity index 99% rename from src/Roboto.Memory/Snapshots/Entity.cs rename to src/Nexus.Memory/Snapshots/Entity.cs index 782acd6..a77a27a 100644 --- a/src/Roboto.Memory/Snapshots/Entity.cs +++ b/src/Nexus.Memory/Snapshots/Entity.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; public enum EntityType { diff --git a/src/Roboto.Memory/Snapshots/GameStateSnapshot.cs b/src/Nexus.Memory/Snapshots/GameStateSnapshot.cs similarity index 98% rename from src/Roboto.Memory/Snapshots/GameStateSnapshot.cs rename to src/Nexus.Memory/Snapshots/GameStateSnapshot.cs index 2a5396c..5622202 100644 --- a/src/Roboto.Memory/Snapshots/GameStateSnapshot.cs +++ b/src/Nexus.Memory/Snapshots/GameStateSnapshot.cs @@ -1,7 +1,7 @@ using System.Numerics; -using Roboto.Memory.Objects; +using Nexus.Memory.Objects; -namespace Roboto.Memory; +namespace Nexus.Memory; public class GameStateSnapshot { diff --git a/src/Roboto.Memory/Snapshots/QuestLinkedEntry.cs b/src/Nexus.Memory/Snapshots/QuestLinkedEntry.cs similarity index 98% rename from src/Roboto.Memory/Snapshots/QuestLinkedEntry.cs rename to src/Nexus.Memory/Snapshots/QuestLinkedEntry.cs index 7c6d285..7574e2d 100644 --- a/src/Roboto.Memory/Snapshots/QuestLinkedEntry.cs +++ b/src/Nexus.Memory/Snapshots/QuestLinkedEntry.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// A quest entry from the GameUi linked lists. diff --git a/src/Roboto.Memory/Snapshots/QuestSnapshot.cs b/src/Nexus.Memory/Snapshots/QuestSnapshot.cs similarity index 88% rename from src/Roboto.Memory/Snapshots/QuestSnapshot.cs rename to src/Nexus.Memory/Snapshots/QuestSnapshot.cs index 3d902f2..20de386 100644 --- a/src/Roboto.Memory/Snapshots/QuestSnapshot.cs +++ b/src/Nexus.Memory/Snapshots/QuestSnapshot.cs @@ -1,8 +1,8 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Lightweight quest data from ServerData quest flags. -/// Stored in GameStateSnapshot; mapped to Roboto.Core.QuestProgress in the Data layer. +/// Stored in GameStateSnapshot; mapped to Nexus.Core.QuestProgress in the Data layer. /// public sealed class QuestSnapshot { diff --git a/src/Roboto.Memory/Snapshots/QuestStateEntry.cs b/src/Nexus.Memory/Snapshots/QuestStateEntry.cs similarity index 93% rename from src/Roboto.Memory/Snapshots/QuestStateEntry.cs rename to src/Nexus.Memory/Snapshots/QuestStateEntry.cs index 8675b0b..1caf5a1 100644 --- a/src/Roboto.Memory/Snapshots/QuestStateEntry.cs +++ b/src/Nexus.Memory/Snapshots/QuestStateEntry.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// A quest state entry from the AreaInstance quest state container. diff --git a/src/Roboto.Memory/Snapshots/SkillSnapshot.cs b/src/Nexus.Memory/Snapshots/SkillSnapshot.cs similarity index 92% rename from src/Roboto.Memory/Snapshots/SkillSnapshot.cs rename to src/Nexus.Memory/Snapshots/SkillSnapshot.cs index 851e54d..cee55a1 100644 --- a/src/Roboto.Memory/Snapshots/SkillSnapshot.cs +++ b/src/Nexus.Memory/Snapshots/SkillSnapshot.cs @@ -1,8 +1,8 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Lightweight skill data from the Actor component's ActiveSkills vector. -/// Stored in GameStateSnapshot; mapped to Roboto.Core.SkillState in the Data layer. +/// Stored in GameStateSnapshot; mapped to Nexus.Core.SkillState in the Data layer. /// public sealed class SkillSnapshot { diff --git a/src/Roboto.Memory/Snapshots/UIElementNode.cs b/src/Nexus.Memory/Snapshots/UIElementNode.cs similarity index 95% rename from src/Roboto.Memory/Snapshots/UIElementNode.cs rename to src/Nexus.Memory/Snapshots/UIElementNode.cs index f7325a4..0b93c1f 100644 --- a/src/Roboto.Memory/Snapshots/UIElementNode.cs +++ b/src/Nexus.Memory/Snapshots/UIElementNode.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// Lightweight snapshot of a single UIElement from the game's UI tree. diff --git a/src/Roboto.Memory/Snapshots/UiQuestEntry.cs b/src/Nexus.Memory/Snapshots/UiQuestEntry.cs similarity index 96% rename from src/Roboto.Memory/Snapshots/UiQuestEntry.cs rename to src/Nexus.Memory/Snapshots/UiQuestEntry.cs index 5516512..8ce2b6d 100644 --- a/src/Roboto.Memory/Snapshots/UiQuestEntry.cs +++ b/src/Nexus.Memory/Snapshots/UiQuestEntry.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; /// /// A quest group from the UI element tree (one per quest_display). diff --git a/src/Roboto.Memory/Snapshots/WalkabilityGrid.cs b/src/Nexus.Memory/Snapshots/WalkabilityGrid.cs similarity index 94% rename from src/Roboto.Memory/Snapshots/WalkabilityGrid.cs rename to src/Nexus.Memory/Snapshots/WalkabilityGrid.cs index 4b52073..22f5453 100644 --- a/src/Roboto.Memory/Snapshots/WalkabilityGrid.cs +++ b/src/Nexus.Memory/Snapshots/WalkabilityGrid.cs @@ -1,4 +1,4 @@ -namespace Roboto.Memory; +namespace Nexus.Memory; public sealed class WalkabilityGrid { diff --git a/src/Automata.Navigation/AtlasPanorama.cs b/src/Nexus.Navigation/AtlasPanorama.cs similarity index 99% rename from src/Automata.Navigation/AtlasPanorama.cs rename to src/Nexus.Navigation/AtlasPanorama.cs index 354baa6..b4a90b8 100644 --- a/src/Automata.Navigation/AtlasPanorama.cs +++ b/src/Nexus.Navigation/AtlasPanorama.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using OpenCvSharp; -using Automata.Core; -using Automata.Screen; +using Nexus.Core; +using Nexus.Screen; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; public record AtlasProgress(int TilesCaptured, int Row, string Phase); diff --git a/src/Automata.Navigation/IconDetector.cs b/src/Nexus.Navigation/IconDetector.cs similarity index 99% rename from src/Automata.Navigation/IconDetector.cs rename to src/Nexus.Navigation/IconDetector.cs index 9312879..a466d36 100644 --- a/src/Automata.Navigation/IconDetector.cs +++ b/src/Nexus.Navigation/IconDetector.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; /// /// Detects minimap icons (doors, checkpoints) via template matching. diff --git a/src/Automata.Navigation/MinimapCapture.cs b/src/Nexus.Navigation/MinimapCapture.cs similarity index 99% rename from src/Automata.Navigation/MinimapCapture.cs rename to src/Nexus.Navigation/MinimapCapture.cs index e7e3093..f0aae8e 100644 --- a/src/Automata.Navigation/MinimapCapture.cs +++ b/src/Nexus.Navigation/MinimapCapture.cs @@ -1,10 +1,10 @@ using OpenCvSharp; -using Automata.Screen; +using Nexus.Screen; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; using Size = OpenCvSharp.Size; -namespace Automata.Navigation; +namespace Nexus.Navigation; public class MinimapCapture : IFrameConsumer, IDisposable { diff --git a/src/Automata.Navigation/NavigationExecutor.cs b/src/Nexus.Navigation/NavigationExecutor.cs similarity index 99% rename from src/Automata.Navigation/NavigationExecutor.cs rename to src/Nexus.Navigation/NavigationExecutor.cs index a179164..da36c11 100644 --- a/src/Automata.Navigation/NavigationExecutor.cs +++ b/src/Nexus.Navigation/NavigationExecutor.cs @@ -1,11 +1,11 @@ using System.Diagnostics; using OpenCvSharp; -using Automata.Core; -using Automata.Game; -using Automata.Screen; +using Nexus.Core; +using Nexus.Game; +using Nexus.Screen; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; public class NavigationExecutor : IDisposable { diff --git a/src/Automata.Navigation/NavigationTypes.cs b/src/Nexus.Navigation/NavigationTypes.cs similarity index 99% rename from src/Automata.Navigation/NavigationTypes.cs rename to src/Nexus.Navigation/NavigationTypes.cs index 65080af..884a833 100644 --- a/src/Automata.Navigation/NavigationTypes.cs +++ b/src/Nexus.Navigation/NavigationTypes.cs @@ -1,7 +1,7 @@ -using Automata.Core; +using Nexus.Core; using OpenCvSharp; -namespace Automata.Navigation; +namespace Nexus.Navigation; public enum MinimapMode { diff --git a/src/Automata.Navigation/Automata.Navigation.csproj b/src/Nexus.Navigation/Nexus.Navigation.csproj similarity index 73% rename from src/Automata.Navigation/Automata.Navigation.csproj rename to src/Nexus.Navigation/Nexus.Navigation.csproj index 29698e6..bd3c70c 100644 --- a/src/Automata.Navigation/Automata.Navigation.csproj +++ b/src/Nexus.Navigation/Nexus.Navigation.csproj @@ -12,8 +12,8 @@ - - - + + + diff --git a/src/Automata.Navigation/PathFinder.cs b/src/Nexus.Navigation/PathFinder.cs similarity index 99% rename from src/Automata.Navigation/PathFinder.cs rename to src/Nexus.Navigation/PathFinder.cs index 9445e81..2988fbd 100644 --- a/src/Automata.Navigation/PathFinder.cs +++ b/src/Nexus.Navigation/PathFinder.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; /// /// Last BFS result for visualization. diff --git a/src/Automata.Navigation/PerspectiveCalibrator.cs b/src/Nexus.Navigation/PerspectiveCalibrator.cs similarity index 99% rename from src/Automata.Navigation/PerspectiveCalibrator.cs rename to src/Nexus.Navigation/PerspectiveCalibrator.cs index 1f3ab8f..8d6da04 100644 --- a/src/Automata.Navigation/PerspectiveCalibrator.cs +++ b/src/Nexus.Navigation/PerspectiveCalibrator.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Automata.Core; -using Automata.Screen; +using Nexus.Core; +using Nexus.Screen; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; public record CalibrationResult(float BestFactor, double BestConfidence, Dictionary AllResults); diff --git a/src/Automata.Navigation/StuckDetector.cs b/src/Nexus.Navigation/StuckDetector.cs similarity index 96% rename from src/Automata.Navigation/StuckDetector.cs rename to src/Nexus.Navigation/StuckDetector.cs index 5dd02c5..3168233 100644 --- a/src/Automata.Navigation/StuckDetector.cs +++ b/src/Nexus.Navigation/StuckDetector.cs @@ -1,4 +1,4 @@ -namespace Automata.Navigation; +namespace Nexus.Navigation; /// /// Detects when the player hasn't moved significantly over a window of frames. diff --git a/src/Automata.Navigation/WallColorTracker.cs b/src/Nexus.Navigation/WallColorTracker.cs similarity index 99% rename from src/Automata.Navigation/WallColorTracker.cs rename to src/Nexus.Navigation/WallColorTracker.cs index a28d810..d9e9896 100644 --- a/src/Automata.Navigation/WallColorTracker.cs +++ b/src/Nexus.Navigation/WallColorTracker.cs @@ -1,7 +1,7 @@ using OpenCvSharp; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; /// /// Tracks HSV distribution of confirmed wall pixels and computes an adaptive diff --git a/src/Automata.Navigation/WorldMap.cs b/src/Nexus.Navigation/WorldMap.cs similarity index 99% rename from src/Automata.Navigation/WorldMap.cs rename to src/Nexus.Navigation/WorldMap.cs index 4435fd5..922cbd2 100644 --- a/src/Automata.Navigation/WorldMap.cs +++ b/src/Nexus.Navigation/WorldMap.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using OpenCvSharp; using Serilog; -namespace Automata.Navigation; +namespace Nexus.Navigation; public class WorldMap : IDisposable { diff --git a/src/Roboto.Navigation/NavigationController.cs b/src/Nexus.Pathfinding/NavigationController.cs similarity index 99% rename from src/Roboto.Navigation/NavigationController.cs rename to src/Nexus.Pathfinding/NavigationController.cs index 4a88f65..831d587 100644 --- a/src/Roboto.Navigation/NavigationController.cs +++ b/src/Nexus.Pathfinding/NavigationController.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Navigation; +namespace Nexus.Pathfinding; public enum NavMode { diff --git a/src/Roboto.Navigation/Roboto.Navigation.csproj b/src/Nexus.Pathfinding/Nexus.Pathfinding.csproj similarity index 83% rename from src/Roboto.Navigation/Roboto.Navigation.csproj rename to src/Nexus.Pathfinding/Nexus.Pathfinding.csproj index bcebb88..60b2b65 100644 --- a/src/Roboto.Navigation/Roboto.Navigation.csproj +++ b/src/Nexus.Pathfinding/Nexus.Pathfinding.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/Roboto.Navigation/PathFinder.cs b/src/Nexus.Pathfinding/PathFinder.cs similarity index 99% rename from src/Roboto.Navigation/PathFinder.cs rename to src/Nexus.Pathfinding/PathFinder.cs index 4b5e4b6..cbc05c9 100644 --- a/src/Roboto.Navigation/PathFinder.cs +++ b/src/Nexus.Pathfinding/PathFinder.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Navigation; +namespace Nexus.Pathfinding; public static class PathFinder { diff --git a/src/Automata.Screen/BossDetector.cs b/src/Nexus.Screen/BossDetector.cs similarity index 99% rename from src/Automata.Screen/BossDetector.cs rename to src/Nexus.Screen/BossDetector.cs index 2fb6f31..c0e80ff 100644 --- a/src/Automata.Screen/BossDetector.cs +++ b/src/Nexus.Screen/BossDetector.cs @@ -1,8 +1,8 @@ using OpenCvSharp; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// Detects bosses using YOLO running on a background thread. diff --git a/src/Automata.Screen/DaemonTypes.cs b/src/Nexus.Screen/DaemonTypes.cs similarity index 97% rename from src/Automata.Screen/DaemonTypes.cs rename to src/Nexus.Screen/DaemonTypes.cs index f149805..fd27194 100644 --- a/src/Automata.Screen/DaemonTypes.cs +++ b/src/Nexus.Screen/DaemonTypes.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Automata.Screen; +namespace Nexus.Screen; public class OcrWord { @@ -49,7 +49,7 @@ public class DiffOcrResponse { public string Text { get; set; } = ""; public List Lines { get; set; } = []; - public Automata.Core.Region? Region { get; set; } + public Nexus.Core.Region? Region { get; set; } } public class TemplateMatchResult diff --git a/src/Automata.Screen/DesktopDuplication.cs b/src/Nexus.Screen/DesktopDuplication.cs similarity index 98% rename from src/Automata.Screen/DesktopDuplication.cs rename to src/Nexus.Screen/DesktopDuplication.cs index 4d81bcf..40b4e39 100644 --- a/src/Automata.Screen/DesktopDuplication.cs +++ b/src/Nexus.Screen/DesktopDuplication.cs @@ -5,9 +5,9 @@ using SharpGen.Runtime; using Vortice.Direct3D; using Vortice.Direct3D11; using Vortice.DXGI; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; public sealed class DesktopDuplication : IScreenCapture { diff --git a/src/Automata.Screen/DetectGridHandler.cs b/src/Nexus.Screen/DetectGridHandler.cs similarity index 98% rename from src/Automata.Screen/DetectGridHandler.cs rename to src/Nexus.Screen/DetectGridHandler.cs index ab4b4f0..581076c 100644 --- a/src/Automata.Screen/DetectGridHandler.cs +++ b/src/Nexus.Screen/DetectGridHandler.cs @@ -1,10 +1,10 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; class DetectGridHandler { diff --git a/src/Automata.Screen/DetectionTypes.cs b/src/Nexus.Screen/DetectionTypes.cs similarity index 95% rename from src/Automata.Screen/DetectionTypes.cs rename to src/Nexus.Screen/DetectionTypes.cs index 2ada4bf..4252710 100644 --- a/src/Automata.Screen/DetectionTypes.cs +++ b/src/Nexus.Screen/DetectionTypes.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; public record DetectedEnemy( float Confidence, diff --git a/src/Automata.Screen/DiffCropHandler.cs b/src/Nexus.Screen/DiffCropHandler.cs similarity index 99% rename from src/Automata.Screen/DiffCropHandler.cs rename to src/Nexus.Screen/DiffCropHandler.cs index acd300f..4b3781a 100644 --- a/src/Automata.Screen/DiffCropHandler.cs +++ b/src/Nexus.Screen/DiffCropHandler.cs @@ -1,10 +1,10 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; class DiffCropHandler { diff --git a/src/Automata.Screen/EdgeCropHandler.cs b/src/Nexus.Screen/EdgeCropHandler.cs similarity index 99% rename from src/Automata.Screen/EdgeCropHandler.cs rename to src/Nexus.Screen/EdgeCropHandler.cs index 6fbe46b..301390f 100644 --- a/src/Automata.Screen/EdgeCropHandler.cs +++ b/src/Nexus.Screen/EdgeCropHandler.cs @@ -1,10 +1,10 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; class EdgeCropHandler { diff --git a/src/Automata.Screen/EnemyDetector.cs b/src/Nexus.Screen/EnemyDetector.cs similarity index 98% rename from src/Automata.Screen/EnemyDetector.cs rename to src/Nexus.Screen/EnemyDetector.cs index 37727bf..2e1406a 100644 --- a/src/Automata.Screen/EnemyDetector.cs +++ b/src/Nexus.Screen/EnemyDetector.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Automata.Core; +using Nexus.Core; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// Detects enemies on screen using two-stage approach: diff --git a/src/Automata.Screen/FramePipeline.cs b/src/Nexus.Screen/FramePipeline.cs similarity index 97% rename from src/Automata.Screen/FramePipeline.cs rename to src/Nexus.Screen/FramePipeline.cs index e8734e5..b5e52cf 100644 --- a/src/Automata.Screen/FramePipeline.cs +++ b/src/Nexus.Screen/FramePipeline.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; public class FramePipeline : IDisposable { diff --git a/src/Automata.Screen/FramePipelineService.cs b/src/Nexus.Screen/FramePipelineService.cs similarity index 96% rename from src/Automata.Screen/FramePipelineService.cs rename to src/Nexus.Screen/FramePipelineService.cs index b213661..42e5362 100644 --- a/src/Automata.Screen/FramePipelineService.cs +++ b/src/Nexus.Screen/FramePipelineService.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Automata.Screen; +namespace Nexus.Screen; public class FramePipelineService : IDisposable { diff --git a/src/Automata.Screen/FrameSaver.cs b/src/Nexus.Screen/FrameSaver.cs similarity index 98% rename from src/Automata.Screen/FrameSaver.cs rename to src/Nexus.Screen/FrameSaver.cs index 2eb2c88..3e7831c 100644 --- a/src/Automata.Screen/FrameSaver.cs +++ b/src/Nexus.Screen/FrameSaver.cs @@ -1,8 +1,8 @@ using OpenCvSharp; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// Saves full-screen frames as JPEGs for YOLO training data collection. diff --git a/src/Automata.Screen/GameStateDetector.cs b/src/Nexus.Screen/GameStateDetector.cs similarity index 95% rename from src/Automata.Screen/GameStateDetector.cs rename to src/Nexus.Screen/GameStateDetector.cs index 89be1bd..5061e17 100644 --- a/src/Automata.Screen/GameStateDetector.cs +++ b/src/Nexus.Screen/GameStateDetector.cs @@ -1,7 +1,7 @@ -using Automata.Core; +using Nexus.Core; using Serilog; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// Classifies the current game UI state by probing known pixel positions on each frame. diff --git a/src/Automata.Screen/GdiCapture.cs b/src/Nexus.Screen/GdiCapture.cs similarity index 95% rename from src/Automata.Screen/GdiCapture.cs rename to src/Nexus.Screen/GdiCapture.cs index ff72148..0a6313c 100644 --- a/src/Automata.Screen/GdiCapture.cs +++ b/src/Nexus.Screen/GdiCapture.cs @@ -3,9 +3,9 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using OpenCvSharp; using OpenCvSharp.Extensions; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; public sealed class GdiCapture : IScreenCapture { diff --git a/src/Automata.Screen/GridHandler.cs b/src/Nexus.Screen/GridHandler.cs similarity index 99% rename from src/Automata.Screen/GridHandler.cs rename to src/Nexus.Screen/GridHandler.cs index 6943bcd..7a5c3ba 100644 --- a/src/Automata.Screen/GridHandler.cs +++ b/src/Nexus.Screen/GridHandler.cs @@ -1,8 +1,8 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; public class GridHandler { diff --git a/src/Automata.Screen/GridReader.cs b/src/Nexus.Screen/GridReader.cs similarity index 98% rename from src/Automata.Screen/GridReader.cs rename to src/Nexus.Screen/GridReader.cs index 0a9285d..8be4fd0 100644 --- a/src/Automata.Screen/GridReader.cs +++ b/src/Nexus.Screen/GridReader.cs @@ -1,7 +1,7 @@ -using Automata.Core; +using Nexus.Core; using Serilog; -namespace Automata.Screen; +namespace Nexus.Screen; public class GridLayout { diff --git a/src/Automata.Screen/HudReader.cs b/src/Nexus.Screen/HudReader.cs similarity index 98% rename from src/Automata.Screen/HudReader.cs rename to src/Nexus.Screen/HudReader.cs index 3e60e93..2fe83ed 100644 --- a/src/Automata.Screen/HudReader.cs +++ b/src/Nexus.Screen/HudReader.cs @@ -1,9 +1,9 @@ using OpenCvSharp; -using Automata.Core; +using Nexus.Core; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; public record HudSnapshot { diff --git a/src/Automata.Screen/IFrameConsumer.cs b/src/Nexus.Screen/IFrameConsumer.cs similarity index 73% rename from src/Automata.Screen/IFrameConsumer.cs rename to src/Nexus.Screen/IFrameConsumer.cs index b7089c6..ca96ebb 100644 --- a/src/Automata.Screen/IFrameConsumer.cs +++ b/src/Nexus.Screen/IFrameConsumer.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; public interface IFrameConsumer { diff --git a/src/Automata.Screen/IOcrEngine.cs b/src/Nexus.Screen/IOcrEngine.cs similarity index 83% rename from src/Automata.Screen/IOcrEngine.cs rename to src/Nexus.Screen/IOcrEngine.cs index 6822e5c..a66bf7b 100644 --- a/src/Automata.Screen/IOcrEngine.cs +++ b/src/Nexus.Screen/IOcrEngine.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Automata.Screen; +namespace Nexus.Screen; public interface IOcrEngine : IDisposable { diff --git a/src/Automata.Screen/IScreenCapture.cs b/src/Nexus.Screen/IScreenCapture.cs similarity index 69% rename from src/Automata.Screen/IScreenCapture.cs rename to src/Nexus.Screen/IScreenCapture.cs index 4e96887..f354f93 100644 --- a/src/Automata.Screen/IScreenCapture.cs +++ b/src/Nexus.Screen/IScreenCapture.cs @@ -1,7 +1,7 @@ using OpenCvSharp; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; public interface IScreenCapture : IDisposable { diff --git a/src/Automata.Screen/IScreenReader.cs b/src/Nexus.Screen/IScreenReader.cs similarity index 96% rename from src/Automata.Screen/IScreenReader.cs rename to src/Nexus.Screen/IScreenReader.cs index 3d5e808..dbeaf98 100644 --- a/src/Automata.Screen/IScreenReader.cs +++ b/src/Nexus.Screen/IScreenReader.cs @@ -1,6 +1,6 @@ -using Automata.Core; +using Nexus.Core; -namespace Automata.Screen; +namespace Nexus.Screen; public interface IScreenReader : IDisposable { diff --git a/src/Automata.Screen/ImagePreprocessor.cs b/src/Nexus.Screen/ImagePreprocessor.cs similarity index 99% rename from src/Automata.Screen/ImagePreprocessor.cs rename to src/Nexus.Screen/ImagePreprocessor.cs index 4e374e6..b935b75 100644 --- a/src/Automata.Screen/ImagePreprocessor.cs +++ b/src/Nexus.Screen/ImagePreprocessor.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using OpenCvSharp; diff --git a/src/Automata.Screen/ImageUtils.cs b/src/Nexus.Screen/ImageUtils.cs similarity index 97% rename from src/Automata.Screen/ImageUtils.cs rename to src/Nexus.Screen/ImageUtils.cs index 8cb346b..dde7e84 100644 --- a/src/Automata.Screen/ImageUtils.cs +++ b/src/Nexus.Screen/ImageUtils.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using System.Drawing.Imaging; diff --git a/src/Automata.Screen/LootDebugDetector.cs b/src/Nexus.Screen/LootDebugDetector.cs similarity index 98% rename from src/Automata.Screen/LootDebugDetector.cs rename to src/Nexus.Screen/LootDebugDetector.cs index 47c2f4c..0a5c7cb 100644 --- a/src/Automata.Screen/LootDebugDetector.cs +++ b/src/Nexus.Screen/LootDebugDetector.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// Debug-only: periodically captures the screen, runs loot label detection, diff --git a/src/Automata.Screen/LootLabel.cs b/src/Nexus.Screen/LootLabel.cs similarity index 99% rename from src/Automata.Screen/LootLabel.cs rename to src/Nexus.Screen/LootLabel.cs index a3fd8d3..99e56eb 100644 --- a/src/Automata.Screen/LootLabel.cs +++ b/src/Nexus.Screen/LootLabel.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; /// /// A detected loot label on screen with its position and classified tier. diff --git a/src/Automata.Screen/Automata.Screen.csproj b/src/Nexus.Screen/Nexus.Screen.csproj similarity index 91% rename from src/Automata.Screen/Automata.Screen.csproj rename to src/Nexus.Screen/Nexus.Screen.csproj index 780d038..e2b7096 100644 --- a/src/Automata.Screen/Automata.Screen.csproj +++ b/src/Nexus.Screen/Nexus.Screen.csproj @@ -15,6 +15,6 @@ - + diff --git a/src/Automata.Screen/Ocr/EasyOcrEngine.cs b/src/Nexus.Screen/Ocr/EasyOcrEngine.cs similarity index 96% rename from src/Automata.Screen/Ocr/EasyOcrEngine.cs rename to src/Nexus.Screen/Ocr/EasyOcrEngine.cs index 47d6655..1a81fd8 100644 --- a/src/Automata.Screen/Ocr/EasyOcrEngine.cs +++ b/src/Nexus.Screen/Ocr/EasyOcrEngine.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Automata.Screen.Ocr; +namespace Nexus.Screen.Ocr; /// /// OCR engine wrapping the Python EasyOCR daemon. diff --git a/src/Automata.Screen/Ocr/OcrEngineFactory.cs b/src/Nexus.Screen/Ocr/OcrEngineFactory.cs similarity index 93% rename from src/Automata.Screen/Ocr/OcrEngineFactory.cs rename to src/Nexus.Screen/Ocr/OcrEngineFactory.cs index 8ee2636..e4cd982 100644 --- a/src/Automata.Screen/Ocr/OcrEngineFactory.cs +++ b/src/Nexus.Screen/Ocr/OcrEngineFactory.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Automata.Screen.Ocr; +namespace Nexus.Screen.Ocr; public static class OcrEngineFactory { diff --git a/src/Automata.Screen/Ocr/OneOcrEngine.cs b/src/Nexus.Screen/Ocr/OneOcrEngine.cs similarity index 99% rename from src/Automata.Screen/Ocr/OneOcrEngine.cs rename to src/Nexus.Screen/Ocr/OneOcrEngine.cs index a982024..dd9a5ec 100644 --- a/src/Automata.Screen/Ocr/OneOcrEngine.cs +++ b/src/Nexus.Screen/Ocr/OneOcrEngine.cs @@ -3,7 +3,7 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using Serilog; -namespace Automata.Screen.Ocr; +namespace Nexus.Screen.Ocr; /// /// OCR engine using OneOCR (Windows 11 Snipping Tool's built-in engine). diff --git a/src/Automata.Screen/Ocr/WinOcrEngine.cs b/src/Nexus.Screen/Ocr/WinOcrEngine.cs similarity index 98% rename from src/Automata.Screen/Ocr/WinOcrEngine.cs rename to src/Nexus.Screen/Ocr/WinOcrEngine.cs index f9688f1..b1e7cd5 100644 --- a/src/Automata.Screen/Ocr/WinOcrEngine.cs +++ b/src/Nexus.Screen/Ocr/WinOcrEngine.cs @@ -7,7 +7,7 @@ using Windows.Storage.Streams; using BitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder; using SdImageFormat = System.Drawing.Imaging.ImageFormat; -namespace Automata.Screen.Ocr; +namespace Nexus.Screen.Ocr; public sealed class WinOcrEngine : IOcrEngine { diff --git a/src/Automata.Screen/OnnxYoloDetector.cs b/src/Nexus.Screen/OnnxYoloDetector.cs similarity index 99% rename from src/Automata.Screen/OnnxYoloDetector.cs rename to src/Nexus.Screen/OnnxYoloDetector.cs index 0ac27bd..0da8399 100644 --- a/src/Automata.Screen/OnnxYoloDetector.cs +++ b/src/Nexus.Screen/OnnxYoloDetector.cs @@ -5,7 +5,7 @@ using OpenCvSharp; using OpenCvSharp.Dnn; using Serilog; -namespace Automata.Screen; +namespace Nexus.Screen; /// /// YOLO11 object detection via ONNX Runtime with CUDA GPU acceleration. diff --git a/src/Automata.Screen/PythonDetectBridge.cs b/src/Nexus.Screen/PythonDetectBridge.cs similarity index 99% rename from src/Automata.Screen/PythonDetectBridge.cs rename to src/Nexus.Screen/PythonDetectBridge.cs index 6a083a3..4249144 100644 --- a/src/Automata.Screen/PythonDetectBridge.cs +++ b/src/Nexus.Screen/PythonDetectBridge.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Diagnostics; using System.Text.Json; diff --git a/src/Automata.Screen/PythonOcrBridge.cs b/src/Nexus.Screen/PythonOcrBridge.cs similarity index 99% rename from src/Automata.Screen/PythonOcrBridge.cs rename to src/Nexus.Screen/PythonOcrBridge.cs index ef206d9..544135f 100644 --- a/src/Automata.Screen/PythonOcrBridge.cs +++ b/src/Nexus.Screen/PythonOcrBridge.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Diagnostics; using System.Drawing; diff --git a/src/Automata.Screen/ScreenCapture.cs b/src/Nexus.Screen/ScreenCapture.cs similarity index 96% rename from src/Automata.Screen/ScreenCapture.cs rename to src/Nexus.Screen/ScreenCapture.cs index 8d64f61..4018ee6 100644 --- a/src/Automata.Screen/ScreenCapture.cs +++ b/src/Nexus.Screen/ScreenCapture.cs @@ -1,9 +1,9 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; static class ScreenCapture { diff --git a/src/Automata.Screen/ScreenFrame.cs b/src/Nexus.Screen/ScreenFrame.cs similarity index 93% rename from src/Automata.Screen/ScreenFrame.cs rename to src/Nexus.Screen/ScreenFrame.cs index 7acceeb..5377be0 100644 --- a/src/Automata.Screen/ScreenFrame.cs +++ b/src/Nexus.Screen/ScreenFrame.cs @@ -1,7 +1,7 @@ using OpenCvSharp; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; -namespace Automata.Screen; +namespace Nexus.Screen; public class ScreenFrame : IDisposable { diff --git a/src/Automata.Screen/ScreenReader.cs b/src/Nexus.Screen/ScreenReader.cs similarity index 99% rename from src/Automata.Screen/ScreenReader.cs rename to src/Nexus.Screen/ScreenReader.cs index c2457a7..3a92ca7 100644 --- a/src/Automata.Screen/ScreenReader.cs +++ b/src/Nexus.Screen/ScreenReader.cs @@ -3,12 +3,12 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; using OpenCvSharp; using OpenCvSharp.Extensions; -using Automata.Core; +using Nexus.Core; using Serilog; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; using Size = OpenCvSharp.Size; -namespace Automata.Screen; +namespace Nexus.Screen; public class ScreenReader : IScreenReader { diff --git a/src/Automata.Screen/SignalProcessing.cs b/src/Nexus.Screen/SignalProcessing.cs similarity index 99% rename from src/Automata.Screen/SignalProcessing.cs rename to src/Nexus.Screen/SignalProcessing.cs index 3f9fee9..84a810d 100644 --- a/src/Automata.Screen/SignalProcessing.cs +++ b/src/Nexus.Screen/SignalProcessing.cs @@ -1,4 +1,4 @@ -namespace Automata.Screen; +namespace Nexus.Screen; static class SignalProcessing { diff --git a/src/Automata.Screen/TemplateMatchHandler.cs b/src/Nexus.Screen/TemplateMatchHandler.cs similarity index 98% rename from src/Automata.Screen/TemplateMatchHandler.cs rename to src/Nexus.Screen/TemplateMatchHandler.cs index d779cf1..61fd1a7 100644 --- a/src/Automata.Screen/TemplateMatchHandler.cs +++ b/src/Nexus.Screen/TemplateMatchHandler.cs @@ -1,9 +1,9 @@ -namespace Automata.Screen; +namespace Nexus.Screen; using System.Drawing; using OpenCvSharp; using OpenCvSharp.Extensions; -using Region = Automata.Core.Region; +using Region = Nexus.Core.Region; class TemplateMatchHandler { diff --git a/src/Nexus.Simulator/Bridge/SimInputController.cs b/src/Nexus.Simulator/Bridge/SimInputController.cs new file mode 100644 index 0000000..794c67e --- /dev/null +++ b/src/Nexus.Simulator/Bridge/SimInputController.cs @@ -0,0 +1,149 @@ +using System.Numerics; +using Nexus.Core; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Bridge; + +/// +/// Captures bot actions (key presses, mouse moves) and feeds them into SimWorld. +/// Implements IInputController so BotEngine systems can emit actions normally. +/// +public class SimInputController : IInputController +{ + private readonly SimWorld _world; + private readonly object _lock = new(); + + // WASD key states + private bool _wHeld, _aHeld, _sHeld, _dHeld; + + // Mouse position (screen coords captured from SmoothMoveTo/MouseMoveTo) + private Vector2 _mouseScreenPos = new(1280, 720); + + // Camera matrix for screen→world conversion + private Matrix4x4? _cameraMatrix; + + public bool IsInitialized => true; + + public SimInputController(SimWorld world) + { + _world = world; + } + + public void SetCameraMatrix(Matrix4x4 camera) + { + _cameraMatrix = camera; + } + + /// + /// Called each sim tick to push accumulated inputs into SimWorld. + /// + public void FlushToWorld() + { + lock (_lock) + { + // Convert WASD to direction vector (with 45° isometric rotation inversion) + var dx = 0f; + var dy = 0f; + if (_wHeld) { dx += 0.70710678f; dy += 0.70710678f; } + if (_sHeld) { dx -= 0.70710678f; dy -= 0.70710678f; } + if (_dHeld) { dx += 0.70710678f; dy -= 0.70710678f; } + if (_aHeld) { dx -= 0.70710678f; dy += 0.70710678f; } + + var dir = new Vector2(dx, dy); + if (dir.LengthSquared() > 0.001f) + dir = Vector2.Normalize(dir); + + _world.MoveDirection = dir; + _world.MouseWorldPos = ScreenToWorld(_mouseScreenPos); + } + } + + private Vector2 ScreenToWorld(Vector2 screen) + { + if (_cameraMatrix is not { } cam) return _world.Player.Position; + if (!Matrix4x4.Invert(cam, out var inv)) return _world.Player.Position; + + // NDC from screen + var ndcX = screen.X / 1280f - 1f; + var ndcY = 1f - screen.Y / 720f; + + var worldNear = Vector4.Transform(new Vector4(ndcX, ndcY, 0, 1), inv); + if (MathF.Abs(worldNear.W) < 0.0001f) return _world.Player.Position; + return new Vector2(worldNear.X / worldNear.W, worldNear.Y / worldNear.W); + } + + // IInputController implementation — captures actions, no actual Win32 calls + + public void KeyDown(ushort scanCode) + { + lock (_lock) + { + switch (scanCode) + { + case 0x11: _wHeld = true; break; // W + case 0x1E: _aHeld = true; break; // A + case 0x1F: _sHeld = true; break; // S + case 0x20: _dHeld = true; break; // D + } + } + } + + public void KeyUp(ushort scanCode) + { + lock (_lock) + { + switch (scanCode) + { + case 0x11: _wHeld = false; break; + case 0x1E: _aHeld = false; break; + case 0x1F: _sHeld = false; break; + case 0x20: _dHeld = false; break; + } + } + } + + public void KeyPress(ushort scanCode, int holdMs = 50) + { + // Queue as skill cast + var target = ScreenToWorld(_mouseScreenPos); + _world.QueueSkill(scanCode, target); + } + + public void MouseMoveTo(int x, int y) + { + lock (_lock) { _mouseScreenPos = new Vector2(x, y); } + } + + public void SmoothMoveTo(int x, int y) => MouseMoveTo(x, y); + public void MouseMoveBy(int dx, int dy) + { + lock (_lock) { _mouseScreenPos += new Vector2(dx, dy); } + } + + public void LeftClick(int x, int y) + { + MouseMoveTo(x, y); + var target = ScreenToWorld(new Vector2(x, y)); + // LMB = default attack / melee + _world.QueueSkill(0, target); + } + + public void RightClick(int x, int y) + { + MouseMoveTo(x, y); + var target = ScreenToWorld(new Vector2(x, y)); + _world.QueueSkill(1, target); + } + + public void MiddleClick(int x, int y) + { + MouseMoveTo(x, y); + var target = ScreenToWorld(new Vector2(x, y)); + _world.QueueSkill(2, target); + } + + public void LeftDown() { } + public void LeftUp() { } + public void RightDown() { } + public void RightUp() { } +} diff --git a/src/Nexus.Simulator/Bridge/SimPoller.cs b/src/Nexus.Simulator/Bridge/SimPoller.cs new file mode 100644 index 0000000..872c569 --- /dev/null +++ b/src/Nexus.Simulator/Bridge/SimPoller.cs @@ -0,0 +1,112 @@ +using System.Numerics; +using Nexus.Core; +using Nexus.Data; +using Nexus.Simulator.Config; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Bridge; + +/// +/// Replaces MemoryPoller. Ticks SimWorld at 60Hz and pushes state to GameDataCache. +/// Runs on a background thread, just like the real MemoryPoller. +/// +public sealed class SimPoller : IDisposable +{ + private readonly SimWorld _world; + private readonly SimInputController _input; + private readonly GameDataCache _cache; + private readonly SimConfig _config; + + private Thread? _thread; + private volatile bool _running; + + public event Action? StateUpdated; + + public SimPoller(SimWorld world, SimInputController input, GameDataCache cache, SimConfig config) + { + _world = world; + _input = input; + _cache = cache; + _config = config; + } + + public void Start() + { + if (_running) return; + _running = true; + _thread = new Thread(PollLoop) + { + Name = "Nexus.SimPoller", + IsBackground = true, + }; + _thread.Start(); + } + + public void Stop() + { + _running = false; + _thread?.Join(2000); + _thread = null; + } + + private void PollLoop() + { + const double targetMs = 1000.0 / 60.0; + var sw = System.Diagnostics.Stopwatch.StartNew(); + var lastMs = sw.Elapsed.TotalMilliseconds; + + while (_running) + { + var nowMs = sw.Elapsed.TotalMilliseconds; + var dt = (float)((nowMs - lastMs) / 1000.0); + lastMs = nowMs; + + // Clamp dt to avoid spiral of death + dt = Math.Min(dt, 0.1f); + + // Flush bot inputs into world + _input.FlushToWorld(); + + // Tick the simulation + _world.Tick(dt); + + // Build GameState + var state = SimStateBuilder.Build(_world, dt); + var cam = SimStateBuilder.BuildCameraMatrix(_world.Player.Position); + + // Update input controller's camera for screen→world conversion + _input.SetCameraMatrix(cam); + + // Push to cache + _cache.CameraMatrix = new CameraMatrixData(cam); + _cache.PlayerPosition = new PlayerPositionData( + _world.Player.Position.X, _world.Player.Position.Y, 0f); + _cache.PlayerVitals = new PlayerVitalsData( + _world.Player.Health, _world.Player.MaxHealth, + _world.Player.Mana, _world.Player.MaxMana, 0, 0); + _cache.IsLoading = false; + _cache.IsEscapeOpen = false; + _cache.Entities = state.Entities; + _cache.HostileMonsters = state.HostileMonsters; + _cache.NearbyLoot = []; + _cache.Terrain = _world.Terrain; + _cache.AreaHash = 1; + _cache.AreaLevel = 1; + _cache.CurrentAreaName = "SimulatedArena"; + _cache.CharacterName = "SimPlayer"; + _cache.LatestState = state; + _cache.HotTickTimestamp = Environment.TickCount64; + _cache.ColdTickTimestamp = Environment.TickCount64; + + StateUpdated?.Invoke(); + + // Sleep for remainder of frame + var elapsed = sw.Elapsed.TotalMilliseconds - nowMs; + var sleep = targetMs - elapsed; + if (sleep > 1) + Thread.Sleep((int)sleep); + } + } + + public void Dispose() => Stop(); +} diff --git a/src/Nexus.Simulator/Bridge/SimStateBuilder.cs b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs new file mode 100644 index 0000000..e209499 --- /dev/null +++ b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs @@ -0,0 +1,115 @@ +using System.Numerics; +using Nexus.Core; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Bridge; + +/// +/// Converts SimWorld state into GameState for the bot systems. +/// +public static class SimStateBuilder +{ + private static long _tickNumber; + + public static GameState Build(SimWorld world, float dt) + { + _tickNumber++; + var player = world.Player; + + var entities = new List(); + var hostiles = new List(); + + foreach (var enemy in world.Enemies) + { + var snap = new EntitySnapshot + { + Id = enemy.Id, + Path = "Metadata/Monsters/SimEnemy", + Category = EntityCategory.Monster, + ThreatLevel = enemy.GetThreatLevel(), + Rarity = enemy.Rarity, + Position = enemy.Position, + Z = 0f, + DistanceToPlayer = Vector2.Distance(enemy.Position, player.Position), + IsAlive = enemy.IsAlive, + LifeCurrent = enemy.Health, + LifeTotal = enemy.MaxHealth, + IsTargetable = enemy.IsAlive, + ActionId = enemy.IsAttacking ? (short)1 : (short)0, + IsAttacking = enemy.IsAttacking, + IsMoving = enemy.AiState == EnemyAiState.Chasing, + }; + entities.Add(snap); + if (enemy.IsAlive) + hostiles.Add(snap); + } + + var cameraMatrix = BuildCameraMatrix(player.Position); + + return new GameState + { + TickNumber = _tickNumber, + DeltaTime = dt, + TimestampMs = Environment.TickCount64, + AreaHash = 1, + AreaLevel = 1, + CurrentAreaName = "SimulatedArena", + IsLoading = false, + IsEscapeOpen = false, + CameraMatrix = cameraMatrix, + Terrain = world.Terrain, + Entities = entities, + HostileMonsters = hostiles, + NearbyLoot = [], + Player = new PlayerState + { + CharacterName = "SimPlayer", + HasPosition = true, + Position = player.Position, + Z = 0f, + LifeCurrent = player.Health, + LifeTotal = player.MaxHealth, + ManaCurrent = player.Mana, + ManaTotal = player.MaxMana, + Skills = BuildSkillStates(), + }, + }; + } + + /// + /// Creates a top-down orthographic camera matrix that WorldToScreen.Project() can use. + /// WorldToScreen expects: sx = (clipX + 1) * 1280, sy = (1 - clipY) * 720 + /// So we need: clipX = (worldX - camX) / halfViewW, clipY = (worldY - camY) / halfViewH + /// + public static Matrix4x4 BuildCameraMatrix(Vector2 playerPos) + { + // View dimensions in world units that map to 2560x1440 screen + // At default zoom, ~5 world units per pixel → 2560*5 = 12800 world units wide + const float viewWidth = 12800f; + const float viewHeight = 7200f; + + // Orthographic projection centered on player + // WorldToScreen does: clip = Transform(world4, matrix), then divides by W + // For ortho: we want (worldX - playerX) / halfW → clipX, (worldY - playerY) / halfH → clipY + var halfW = viewWidth / 2f; + var halfH = viewHeight / 2f; + + // Row-major matrix that transforms (worldX, worldY, 0, 1) → (clipX, clipY, clipZ, 1) + return new Matrix4x4( + 1f / halfW, 0, 0, 0, + 0, 1f / halfH, 0, 0, + 0, 0, 1, 0, + -playerPos.X / halfW, -playerPos.Y / halfH, 0, 1); + } + + private static List BuildSkillStates() + { + return + [ + new SkillState { SlotIndex = 0, Name = "MeleeStrike", SkillBarSlot = 0, CanBeUsed = true, ChargesMax = 1, ChargesCurrent = 1 }, + new SkillState { SlotIndex = 1, Name = "PowerStrike", SkillBarSlot = 1, CanBeUsed = true, ChargesMax = 1, ChargesCurrent = 1 }, + new SkillState { SlotIndex = 3, Name = "WhirlingSlash", SkillBarSlot = 3, CanBeUsed = true, ChargesMax = 1, ChargesCurrent = 1, CooldownTimeMs = 500 }, + new SkillState { SlotIndex = 4, Name = "SpearThrow", SkillBarSlot = 4, CanBeUsed = true, ChargesMax = 1, ChargesCurrent = 1, CooldownTimeMs = 300 }, + ]; + } +} diff --git a/src/Nexus.Simulator/Config/SimConfig.cs b/src/Nexus.Simulator/Config/SimConfig.cs new file mode 100644 index 0000000..7231c97 --- /dev/null +++ b/src/Nexus.Simulator/Config/SimConfig.cs @@ -0,0 +1,47 @@ +namespace Nexus.Simulator.Config; + +public class SimConfig +{ + // Terrain + public int TerrainWidth { get; set; } = 500; + public int TerrainHeight { get; set; } = 500; + public float WorldToGrid { get; set; } = 23f / 250f; + + // Player + public float PlayerMoveSpeed { get; set; } = 400f; + public int PlayerMaxHealth { get; set; } = 1000; + public int PlayerMaxMana { get; set; } = 500; + public float PlayerHealthRegen { get; set; } = 5f; + public float PlayerManaRegen { get; set; } = 10f; + + // Enemies + public int TargetEnemyCount { get; set; } = 25; + public float EnemyAggroRange { get; set; } = 600f; + public float EnemyAttackRange { get; set; } = 100f; + public float EnemyMoveSpeedFactor { get; set; } = 0.75f; + public int EnemyBaseHealth { get; set; } = 200; + public int EnemyAttackDamage { get; set; } = 30; + public float EnemyAttackCooldown { get; set; } = 1.5f; + public float EnemyDespawnTime { get; set; } = 2f; + public float EnemyRespawnTime { get; set; } = 5f; + public float EnemyWanderRadius { get; set; } = 200f; + + // Skills + public float MeleeRange { get; set; } = 150f; + public float MeleeConeAngle { get; set; } = 120f; + public float AoeRadius { get; set; } = 250f; + public float ProjectileSpeed { get; set; } = 1200f; + public float ProjectileRange { get; set; } = 800f; + public float ProjectileHitRadius { get; set; } = 80f; + public int SkillBaseDamage { get; set; } = 200; + + // Simulation + public float SpeedMultiplier { get; set; } = 1f; + public bool IsPaused { get; set; } + + // Rarity distribution (must sum to 1.0) + public float NormalChance { get; set; } = 0.70f; + public float MagicChance { get; set; } = 0.20f; + public float RareChance { get; set; } = 0.08f; + public float UniqueChance { get; set; } = 0.02f; +} diff --git a/src/Nexus.Simulator/Nexus.Simulator.csproj b/src/Nexus.Simulator/Nexus.Simulator.csproj new file mode 100644 index 0000000..9161be8 --- /dev/null +++ b/src/Nexus.Simulator/Nexus.Simulator.csproj @@ -0,0 +1,23 @@ + + + Exe + net8.0-windows10.0.19041.0 + enable + enable + true + + + + + + + + + + + + + + + + diff --git a/src/Nexus.Simulator/Program.cs b/src/Nexus.Simulator/Program.cs new file mode 100644 index 0000000..30330f0 --- /dev/null +++ b/src/Nexus.Simulator/Program.cs @@ -0,0 +1,264 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Core; +using Nexus.Data; +using Nexus.Pathfinding; +using Nexus.Simulator.Bridge; +using Nexus.Simulator.Config; +using Nexus.Simulator.Rendering; +using Nexus.Simulator.World; +using Nexus.Systems; +using Serilog; +using Veldrid; +using Veldrid.Sdl2; +using Veldrid.StartupUtilities; + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + +Log.Information("Nexus Simulator starting..."); + +// ── Configuration ── +var simConfig = new SimConfig(); +var botConfig = new BotConfig +{ + WorldToGrid = simConfig.WorldToGrid, + LogicTickRateHz = 60, +}; + +// ── Create simulation world ── +var world = new SimWorld(simConfig); +var cache = new GameDataCache(); +var input = new SimInputController(world); +var poller = new SimPoller(world, input, cache, simConfig); +var nav = new NavigationController(botConfig); + +// ── Create systems (same as BotEngine, minus AreaProgression) ── +var systems = new List +{ + new ThreatSystem { WorldToGrid = botConfig.WorldToGrid }, + new MovementSystem + { + SafeDistance = botConfig.SafeDistance, + RepulsionWeight = botConfig.RepulsionWeight, + WorldToGrid = botConfig.WorldToGrid, + }, + new NavigationSystem + { + WorldToGrid = botConfig.WorldToGrid, + WaypointReachedDistance = botConfig.WaypointReachedDistance, + }, + new CombatSystem(botConfig), + new ResourceSystem(botConfig), + new LootSystem(), +}; + +// Apply a default profile with configured skills +var profile = new CharacterProfile +{ + Name = "SimPlayer", + Skills = + [ + new() { SlotIndex = 0, Label = "LMB", InputType = SkillInputType.LeftClick, Priority = 0, RangeMax = 150f, RequiresTarget = true }, + new() { SlotIndex = 1, Label = "RMB", InputType = SkillInputType.RightClick, Priority = 1, RangeMax = 150f, RequiresTarget = true }, + new() { SlotIndex = 3, Label = "Q", InputType = SkillInputType.KeyPress, ScanCode = 0x10, Priority = 3, RangeMax = 600f, CooldownMs = 500, MinMonstersInRange = 2 }, + new() { SlotIndex = 4, Label = "E", InputType = SkillInputType.KeyPress, ScanCode = 0x12, Priority = 4, RangeMax = 800f, CooldownMs = 300 }, + ], + Combat = new CombatSettings + { + GlobalCooldownMs = 400, + AttackRange = 600f, + }, +}; + +// Apply profile to systems that need it +foreach (var sys in systems) +{ + if (sys is CombatSystem combat) combat.ApplyProfile(profile); + if (sys is ResourceSystem resource) resource.ApplyProfile(profile); +} + +// ── Start simulation poller ── +poller.Start(); + +// ── Start exploring ── +nav.Explore(); + +// ── Bot logic thread ── +var actionQueue = new ActionQueue(); +var botRunning = true; + +var botThread = new Thread(() => +{ + var sw = System.Diagnostics.Stopwatch.StartNew(); + var intervalMs = 1000.0 / botConfig.LogicTickRateHz; + + while (botRunning) + { + try + { + var state = cache.LatestState; + if (state is not null && !state.IsLoading && !state.IsEscapeOpen) + { + // Enrich + GameStateEnricher.Enrich(state); + + // Clear and run systems + actionQueue.Clear(); + nav.Update(state); + + foreach (var sys in systems) + { + if (sys.IsEnabled) + sys.Update(state, actionQueue); + } + + // Nav direction + if (nav.DesiredDirection.HasValue) + actionQueue.Submit(new MoveAction(SystemPriority.Navigation, nav.DesiredDirection.Value)); + + // Resolve and execute + var resolved = actionQueue.Resolve(); + ExecuteActions(resolved, state, input); + } + } + catch (Exception ex) + { + Log.Debug(ex, "Bot logic error"); + } + + var elapsed = sw.Elapsed.TotalMilliseconds; + var sleep = intervalMs - (elapsed % intervalMs); + if (sleep > 1) + Thread.Sleep((int)sleep); + } +}) +{ + Name = "Nexus.BotLogic", + IsBackground = true, +}; +botThread.Start(); + +// ── Veldrid + ImGui window ── +var windowInfo = new WindowCreateInfo +{ + X = 100, + Y = 100, + WindowWidth = 1600, + WindowHeight = 1000, + WindowTitle = "Nexus Simulator", +}; + +var window = VeldridStartup.CreateWindow(ref windowInfo); +var gd = VeldridStartup.CreateGraphicsDevice(window, new GraphicsDeviceOptions +{ + PreferStandardClipSpaceYDirection = true, + PreferDepthRangeZeroToOne = true, + SyncToVerticalBlank = true, +}, GraphicsBackend.Direct3D11); + +var imguiRenderer = new VeldridImGuiRenderer( + gd, gd.MainSwapchain.Framebuffer.OutputDescription, + window.Width, window.Height); + +var renderer = new SimRenderer(simConfig, world, nav, systems); +var cl = gd.ResourceFactory.CreateCommandList(); + +window.Resized += () => +{ + gd.MainSwapchain.Resize((uint)window.Width, (uint)window.Height); + imguiRenderer.WindowResized(window.Width, window.Height); +}; + +// ── Main render loop ── +var renderSw = System.Diagnostics.Stopwatch.StartNew(); +var lastRenderMs = 0.0; + +while (window.Exists) +{ + var nowMs = renderSw.Elapsed.TotalMilliseconds; + var deltaSeconds = (float)((nowMs - lastRenderMs) / 1000.0); + lastRenderMs = nowMs; + + var snapshot = window.PumpEvents(); + if (!window.Exists) break; + + imguiRenderer.Update(deltaSeconds, snapshot); + + // Render sim world + renderer.Render(cache.LatestState); + + cl.Begin(); + cl.SetFramebuffer(gd.MainSwapchain.Framebuffer); + cl.ClearColorTarget(0, new RgbaFloat(0.05f, 0.05f, 0.08f, 1f)); + imguiRenderer.Render(gd, cl); + cl.End(); + gd.SubmitCommands(cl); + gd.SwapBuffers(gd.MainSwapchain); +} + +// ── Cleanup ── +botRunning = false; +botThread.Join(2000); +poller.Dispose(); +cl.Dispose(); +imguiRenderer.Dispose(); +gd.Dispose(); +Log.Information("Nexus Simulator stopped."); + +// ── Helper: Execute bot actions via SimInputController ── +static void ExecuteActions(List resolved, GameState state, SimInputController input) +{ + foreach (var action in resolved) + { + switch (action) + { + case MoveAction move: + // MovementKeyTracker equivalent — convert direction to WASD key presses + // The SimInputController interprets these directly + const float cos45 = 0.70710678f; + const float sin45 = 0.70710678f; + var sx = move.Direction.X * cos45 - move.Direction.Y * sin45; + var sy = move.Direction.X * sin45 + move.Direction.Y * cos45; + + const float threshold = 0.3f; + if (sy > threshold) input.KeyDown(0x11); else input.KeyUp(0x11); // W + if (sy < -threshold) input.KeyDown(0x1F); else input.KeyUp(0x1F); // S + if (sx > threshold) input.KeyDown(0x20); else input.KeyUp(0x20); // D + if (sx < -threshold) input.KeyDown(0x1E); else input.KeyUp(0x1E); // A + break; + + case CastAction cast: + if (cast.TargetScreenPos.HasValue) + input.SmoothMoveTo((int)cast.TargetScreenPos.Value.X, (int)cast.TargetScreenPos.Value.Y); + input.KeyPress(cast.SkillScanCode); + break; + + case FlaskAction flask: + input.KeyPress(flask.FlaskScanCode); + break; + + case ClickAction click: + var cx = (int)click.ScreenPosition.X; + var cy = (int)click.ScreenPosition.Y; + switch (click.Type) + { + case ClickType.Left: input.LeftClick(cx, cy); break; + case ClickType.Right: input.RightClick(cx, cy); break; + case ClickType.Middle: input.MiddleClick(cx, cy); break; + } + break; + + case KeyAction key: + switch (key.Type) + { + case KeyActionType.Press: input.KeyPress(key.ScanCode); break; + case KeyActionType.Down: input.KeyDown(key.ScanCode); break; + case KeyActionType.Up: input.KeyUp(key.ScanCode); break; + } + break; + } + } +} diff --git a/src/Nexus.Simulator/Rendering/DebugPanel.cs b/src/Nexus.Simulator/Rendering/DebugPanel.cs new file mode 100644 index 0000000..cbb586b --- /dev/null +++ b/src/Nexus.Simulator/Rendering/DebugPanel.cs @@ -0,0 +1,131 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Core; +using Nexus.Pathfinding; +using Nexus.Simulator.Config; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Rendering; + +public class DebugPanel +{ + private readonly SimConfig _config; + private readonly SimWorld _world; + private readonly NavigationController _nav; + private readonly IReadOnlyList _systems; + + private int _spawnRarity; // 0=Normal, 1=Magic, 2=Rare, 3=Unique + + public DebugPanel(SimConfig config, SimWorld world, NavigationController nav, IReadOnlyList systems) + { + _config = config; + _world = world; + _nav = nav; + _systems = systems; + } + + public void Draw(GameState? state) + { + ImGui.Begin("Simulator Controls"); + + // Simulation controls + if (ImGui.CollapsingHeader("Simulation", ImGuiTreeNodeFlags.DefaultOpen)) + { + var paused = _config.IsPaused; + if (ImGui.Checkbox("Paused", ref paused)) + _config.IsPaused = paused; + ImGui.SameLine(); + if (ImGui.Button("Step")) + { + _config.IsPaused = false; + } + + var speed = _config.SpeedMultiplier; + if (ImGui.SliderFloat("Speed", ref speed, 0.1f, 4f, "%.1fx")) + _config.SpeedMultiplier = speed; + + if (ImGui.Button("Regenerate Terrain")) + _world.RegenerateTerrain(); + } + + // Player stats + if (ImGui.CollapsingHeader("Player", ImGuiTreeNodeFlags.DefaultOpen)) + { + var player = _world.Player; + ImGui.Text($"Position: ({player.Position.X:F0}, {player.Position.Y:F0})"); + + var hpPct = player.MaxHealth > 0 ? (float)player.Health / player.MaxHealth : 0; + ImGui.ProgressBar(hpPct, new Vector2(-1, 0), $"HP: {player.Health}/{player.MaxHealth}"); + + var manaPct = player.MaxMana > 0 ? (float)player.Mana / player.MaxMana : 0; + ImGui.ProgressBar(manaPct, new Vector2(-1, 0), $"MP: {player.Mana}/{player.MaxMana}"); + } + + // Enemy stats + if (ImGui.CollapsingHeader("Enemies", ImGuiTreeNodeFlags.DefaultOpen)) + { + var alive = _world.Enemies.Count(e => e.IsAlive); + var dead = _world.Enemies.Count(e => !e.IsAlive); + var chasing = _world.Enemies.Count(e => e.AiState == EnemyAiState.Chasing); + var attacking = _world.Enemies.Count(e => e.AiState == EnemyAiState.Attacking); + + ImGui.Text($"Total: {_world.Enemies.Count} Alive: {alive} Dead: {dead}"); + ImGui.Text($"Chasing: {chasing} Attacking: {attacking}"); + + ImGui.Separator(); + ImGui.Text("Spawn Enemy:"); + string[] rarities = ["Normal", "Magic", "Rare", "Unique"]; + ImGui.Combo("Rarity", ref _spawnRarity, rarities, rarities.Length); + if (ImGui.Button("Spawn at Player")) + { + var rarity = (MonsterRarity)_spawnRarity; + var offset = new Vector2(200, 0); + _world.SpawnEnemyAt(_world.Player.Position + offset, rarity); + } + } + + // Navigation + if (ImGui.CollapsingHeader("Navigation")) + { + ImGui.Text($"Mode: {_nav.Mode}"); + ImGui.Text($"Status: {_nav.Status}"); + ImGui.Text($"Direction: {(_nav.DesiredDirection.HasValue ? $"({_nav.DesiredDirection.Value.X:F2}, {_nav.DesiredDirection.Value.Y:F2})" : "none")}"); + + var path = _nav.CurrentPath; + ImGui.Text($"Path: {path?.Count ?? 0} waypoints"); + ImGui.Text($"Exploration complete: {_nav.IsExplorationComplete}"); + } + + // Systems + if (ImGui.CollapsingHeader("Systems")) + { + foreach (var sys in _systems) + { + var enabled = sys.IsEnabled; + if (ImGui.Checkbox(sys.Name, ref enabled)) + sys.IsEnabled = enabled; + } + } + + // Threat info + if (state is not null && ImGui.CollapsingHeader("Threat")) + { + ImGui.Text($"Danger: {state.Danger}"); + var threats = state.Threats; + ImGui.Text($"Close: {threats.CloseRange} Mid: {threats.MidRange} Far: {threats.FarRange}"); + ImGui.Text($"Closest: {threats.ClosestDistance:F0}"); + ImGui.Text($"Has Rare/Unique: {threats.HasRareOrUnique}"); + } + + // Action queue + if (state is not null && ImGui.CollapsingHeader("State")) + { + ImGui.Text($"Tick: {state.TickNumber}"); + ImGui.Text($"Entities: {state.Entities.Count}"); + ImGui.Text($"Hostile: {state.HostileMonsters.Count}"); + ImGui.Text($"Nearest: {state.NearestEnemies.Count}"); + } + + ImGui.End(); + } +} diff --git a/src/Nexus.Simulator/Rendering/EffectRenderer.cs b/src/Nexus.Simulator/Rendering/EffectRenderer.cs new file mode 100644 index 0000000..45a8cb6 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/EffectRenderer.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Rendering; + +public static class EffectRenderer +{ + public static void DrawEffects(ImDrawListPtr drawList, List effects, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + { + foreach (var effect in effects) + { + var alpha = (byte)(255 * (1f - effect.Progress)); + var originScreen = canvasOrigin + viewOffset + effect.Origin * worldToGrid * zoom; + var targetScreen = canvasOrigin + viewOffset + effect.TargetPosition * worldToGrid * zoom; + + switch (effect.Type) + { + case SkillEffectType.Melee: + DrawMeleeCone(drawList, originScreen, targetScreen, + effect.Radius * worldToGrid * zoom, effect.ConeAngle, alpha); + break; + + case SkillEffectType.Aoe: + DrawAoeCircle(drawList, targetScreen, + effect.Radius * worldToGrid * zoom, alpha); + break; + + case SkillEffectType.Projectile: + DrawProjectileLine(drawList, originScreen, targetScreen, alpha); + break; + } + } + } + + public static void DrawProjectiles(ImDrawListPtr drawList, List projectiles, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + { + foreach (var proj in projectiles) + { + var pos = canvasOrigin + viewOffset + proj.Position * worldToGrid * zoom; + var radius = proj.HitRadius * worldToGrid * zoom * 0.3f; + drawList.AddCircleFilled(pos, Math.Max(3f, radius), 0xFF00DDFF); + drawList.AddCircle(pos, Math.Max(4f, radius + 1), 0xFF00AAFF); + } + } + + private static void DrawMeleeCone(ImDrawListPtr drawList, Vector2 origin, Vector2 target, + float radius, float coneAngle, byte alpha) + { + var dir = target - origin; + if (dir.LengthSquared() < 1f) return; + dir = Vector2.Normalize(dir); + + var halfAngle = coneAngle * MathF.PI / 360f; + var segments = 12; + var color = (uint)(alpha << 24) | 0x00FF4444; + + // Draw cone as triangle fan + var prevPoint = origin + Rotate(dir, -halfAngle) * radius; + for (var i = 1; i <= segments; i++) + { + var t = (float)i / segments; + var angle = -halfAngle + t * halfAngle * 2; + var point = origin + Rotate(dir, angle) * radius; + drawList.AddTriangleFilled(origin, prevPoint, point, color); + prevPoint = point; + } + } + + private static void DrawAoeCircle(ImDrawListPtr drawList, Vector2 center, float radius, byte alpha) + { + var fillColor = (uint)(alpha / 2 << 24) | 0x004488FF; + var borderColor = (uint)(alpha << 24) | 0x006688FF; + drawList.AddCircleFilled(center, radius, fillColor); + drawList.AddCircle(center, radius, borderColor, 0, 2f); + } + + private static void DrawProjectileLine(ImDrawListPtr drawList, Vector2 from, Vector2 to, byte alpha) + { + var color = (uint)(alpha << 24) | 0x0000DDFF; + drawList.AddLine(from, to, color, 2f); + } + + private static Vector2 Rotate(Vector2 v, float radians) + { + var cos = MathF.Cos(radians); + var sin = MathF.Sin(radians); + return new Vector2(v.X * cos - v.Y * sin, v.X * sin + v.Y * cos); + } +} diff --git a/src/Nexus.Simulator/Rendering/EntityRenderer.cs b/src/Nexus.Simulator/Rendering/EntityRenderer.cs new file mode 100644 index 0000000..52aaac1 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/EntityRenderer.cs @@ -0,0 +1,86 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Core; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Rendering; + +public static class EntityRenderer +{ + public static void DrawPlayer(ImDrawListPtr drawList, SimPlayer player, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + { + var gridPos = player.Position * worldToGrid; + var screenPos = canvasOrigin + viewOffset + gridPos * zoom; + + var radius = 8f; + drawList.AddCircleFilled(screenPos, radius, 0xFF00FF00); // Green + drawList.AddCircle(screenPos, radius + 1, 0xFF00AA00); + + // Health bar above player + DrawHealthBar(drawList, screenPos - new Vector2(15, radius + 8), 30, 4, + player.Health, player.MaxHealth, 0xFF00DD00); + + // Mana bar + DrawHealthBar(drawList, screenPos - new Vector2(15, radius + 14), 30, 3, + player.Mana, player.MaxMana, 0xFFDD6600); + } + + public static void DrawEnemies(ImDrawListPtr drawList, List enemies, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid, + Vector2 canvasMin, Vector2 canvasMax) + { + foreach (var enemy in enemies) + { + var gridPos = enemy.Position * worldToGrid; + var screenPos = canvasOrigin + viewOffset + gridPos * zoom; + + // Cull off-screen + if (screenPos.X < canvasMin.X - 20 || screenPos.X > canvasMax.X + 20 || + screenPos.Y < canvasMin.Y - 20 || screenPos.Y > canvasMax.Y + 20) + continue; + + var (color, radius) = enemy.Rarity switch + { + MonsterRarity.Unique => (0xFF00AAFF, 10f), // Orange + MonsterRarity.Rare => (0xFF00FFFF, 8f), // Yellow + MonsterRarity.Magic => (0xFFFF8800, 6f), // Blue + _ => (0xFFCCCCCC, 5f), // White + }; + + if (!enemy.IsAlive) + { + color = 0xFF444444; + radius *= 0.7f; + } + + drawList.AddCircleFilled(screenPos, radius, color); + + if (enemy.AiState == EnemyAiState.Chasing) + drawList.AddCircle(screenPos, radius + 2, 0xFF0000FF); // Red ring when chasing + else if (enemy.AiState == EnemyAiState.Attacking) + drawList.AddCircle(screenPos, radius + 3, 0xFF0000FF, 0, 3f); + + // Health bar + if (enemy.IsAlive && enemy.Health < enemy.MaxHealth) + { + DrawHealthBar(drawList, screenPos - new Vector2(12, radius + 6), 24, 3, + enemy.Health, enemy.MaxHealth, 0xFF0000FF); + } + } + } + + private static void DrawHealthBar(ImDrawListPtr drawList, Vector2 pos, float width, float height, + int current, int max, uint color) + { + if (max <= 0) return; + var pct = (float)current / max; + + // Background + drawList.AddRectFilled(pos, pos + new Vector2(width, height), 0xFF000000); + // Fill + drawList.AddRectFilled(pos, pos + new Vector2(width * pct, height), color); + // Border + drawList.AddRect(pos, pos + new Vector2(width, height), 0xFF666666); + } +} diff --git a/src/Nexus.Simulator/Rendering/PathRenderer.cs b/src/Nexus.Simulator/Rendering/PathRenderer.cs new file mode 100644 index 0000000..66335b7 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/PathRenderer.cs @@ -0,0 +1,37 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Pathfinding; + +namespace Nexus.Simulator.Rendering; + +public static class PathRenderer +{ + public static void Draw(ImDrawListPtr drawList, NavigationController nav, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + { + var path = nav.CurrentPath; + if (path is null || path.Count < 2) return; + + for (var i = 0; i < path.Count - 1; i++) + { + var a = canvasOrigin + viewOffset + path[i] * worldToGrid * zoom; + var b = canvasOrigin + viewOffset + path[i + 1] * worldToGrid * zoom; + drawList.AddLine(a, b, 0xFFFFFF00, 2f); // Cyan + } + + // Draw waypoint dots + foreach (var wp in path) + { + var pos = canvasOrigin + viewOffset + wp * worldToGrid * zoom; + drawList.AddCircleFilled(pos, 3f, 0xFFFFFF00); + } + } + + public static void DrawExploredOverlay(ImDrawListPtr drawList, + bool[]? exploredGrid, int exploredWidth, int exploredHeight, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, Vector2 canvasSize) + { + // Already handled in TerrainRenderer via brightness difference + // This method is a placeholder for additional explore visualization + } +} diff --git a/src/Nexus.Simulator/Rendering/SimRenderer.cs b/src/Nexus.Simulator/Rendering/SimRenderer.cs new file mode 100644 index 0000000..c8bae7c --- /dev/null +++ b/src/Nexus.Simulator/Rendering/SimRenderer.cs @@ -0,0 +1,134 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Core; +using Nexus.Pathfinding; +using Nexus.Simulator.Config; +using Nexus.Simulator.World; + +namespace Nexus.Simulator.Rendering; + +/// +/// Main renderer: draws the top-down game world viewport using ImGui draw lists. +/// +public class SimRenderer +{ + private readonly SimConfig _config; + private readonly SimWorld _world; + private readonly NavigationController _nav; + private readonly DebugPanel _debugPanel; + + // Camera + private Vector2 _viewOffset; + private float _zoom = 2f; // pixels per grid cell + + public SimRenderer(SimConfig config, SimWorld world, NavigationController nav, + IReadOnlyList systems) + { + _config = config; + _world = world; + _nav = nav; + _debugPanel = new DebugPanel(config, world, nav, systems); + + // Center view on player + CenterOnPlayer(); + } + + public void Render(GameState? state) + { + // Debug panel (side window) + _debugPanel.Draw(state); + + // Main viewport + ImGui.SetNextWindowPos(Vector2.Zero, ImGuiCond.FirstUseEver); + ImGui.SetNextWindowSize(new Vector2(1200, 900), ImGuiCond.FirstUseEver); + ImGui.Begin("Simulator", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse); + + var canvasOrigin = ImGui.GetCursorScreenPos(); + var canvasSize = ImGui.GetContentRegionAvail(); + + // Handle input + HandleInput(canvasOrigin, canvasSize); + + // Center camera on player + CenterOnPlayer(canvasSize); + + var drawList = ImGui.GetWindowDrawList(); + + // Clip to canvas + drawList.PushClipRect(canvasOrigin, canvasOrigin + canvasSize); + + // 1. Terrain + TerrainRenderer.Draw(drawList, _world.Terrain, _viewOffset, _zoom, canvasOrigin, canvasSize, + _nav.ExploredGrid, _nav.ExploredWidth, _nav.ExploredHeight); + + // 2. Path + PathRenderer.Draw(drawList, _nav, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + + // 3. Effects + EffectRenderer.DrawEffects(drawList, _world.ActiveEffects, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + EffectRenderer.DrawProjectiles(drawList, _world.Projectiles, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + + // 4. Enemies + EntityRenderer.DrawEnemies(drawList, _world.Enemies, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid, + canvasOrigin, canvasOrigin + canvasSize); + + // 5. Player + EntityRenderer.DrawPlayer(drawList, _world.Player, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + + drawList.PopClipRect(); + + // Minimap (bottom-right corner) + var minimapSize = 150f; + var minimapOrigin = canvasOrigin + canvasSize - new Vector2(minimapSize + 10, minimapSize + 10); + var playerGridPos = _world.Player.Position * _config.WorldToGrid; + TerrainRenderer.DrawMinimap(drawList, _world.Terrain, playerGridPos, minimapOrigin, minimapSize); + + // HUD text + DrawHud(drawList, canvasOrigin, state); + + ImGui.End(); + } + + private void CenterOnPlayer(Vector2? canvasSize = null) + { + var cs = canvasSize ?? new Vector2(1200, 900); + var playerGrid = _world.Player.Position * _config.WorldToGrid; + _viewOffset = cs * 0.5f - playerGrid * _zoom; + } + + private void HandleInput(Vector2 canvasOrigin, Vector2 canvasSize) + { + if (!ImGui.IsWindowHovered()) return; + + var io = ImGui.GetIO(); + + // Scroll to zoom + if (io.MouseWheel != 0) + { + var mousePos = io.MousePos - canvasOrigin; + var worldBeforeZoom = (mousePos - _viewOffset) / _zoom; + + _zoom *= io.MouseWheel > 0 ? 1.15f : 1f / 1.15f; + _zoom = Math.Clamp(_zoom, 0.2f, 20f); + + _viewOffset = mousePos - worldBeforeZoom * _zoom; + } + } + + private void DrawHud(ImDrawListPtr drawList, Vector2 canvasOrigin, GameState? state) + { + var textPos = canvasOrigin + new Vector2(10, 10); + var color = 0xFFFFFFFF; + + drawList.AddText(textPos, color, $"Nav: {_nav.Mode} - {_nav.Status}"); + textPos.Y += 16; + + if (state is not null) + { + drawList.AddText(textPos, color, $"Danger: {state.Danger} Enemies: {state.HostileMonsters.Count}"); + textPos.Y += 16; + } + + drawList.AddText(textPos, color, $"Zoom: {_zoom:F1}x Tick: {_world.TickNumber}"); + } +} diff --git a/src/Nexus.Simulator/Rendering/TerrainRenderer.cs b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs new file mode 100644 index 0000000..6722a74 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs @@ -0,0 +1,99 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Core; + +namespace Nexus.Simulator.Rendering; + +public static class TerrainRenderer +{ + /// + /// Draws the walkability grid as colored rectangles on the ImGui draw list. + /// Only draws cells visible in the current viewport for performance. + /// + public static void Draw(ImDrawListPtr drawList, WalkabilitySnapshot terrain, + Vector2 viewOffset, float zoom, Vector2 canvasOrigin, Vector2 canvasSize, + bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0) + { + var cellSize = zoom; + if (cellSize < 0.5f) return; // Too zoomed out to draw individual cells + + // Visible range in grid coords + var minGx = Math.Max(0, (int)(-viewOffset.X / cellSize)); + var minGy = Math.Max(0, (int)(-viewOffset.Y / cellSize)); + var maxGx = Math.Min(terrain.Width - 1, (int)((-viewOffset.X + canvasSize.X) / cellSize)); + var maxGy = Math.Min(terrain.Height - 1, (int)((-viewOffset.Y + canvasSize.Y) / cellSize)); + + // Skip pixels if too many cells + var step = 1; + if (cellSize < 2f) step = 4; + else if (cellSize < 4f) step = 2; + + for (var gy = minGy; gy <= maxGy; gy += step) + for (var gx = minGx; gx <= maxGx; gx += step) + { + var screenX = canvasOrigin.X + viewOffset.X + gx * cellSize; + var screenY = canvasOrigin.Y + viewOffset.Y + gy * cellSize; + + var w = terrain.IsWalkable(gx, gy); + uint color; + + if (!w) + { + color = 0xFF1A1A2E; // Dark wall + } + else + { + var explored = exploredGrid is not null + && gx < exploredWidth && gy < exploredHeight + && exploredGrid[gy * exploredWidth + gx]; + + color = explored ? 0xFF3D3D5C : 0xFF2A2A3F; // Brighter if explored + } + + var size = cellSize * step; + drawList.AddRectFilled( + new Vector2(screenX, screenY), + new Vector2(screenX + size, screenY + size), + color); + } + } + + /// + /// Draws a minimap in the corner. + /// + public static void DrawMinimap(ImDrawListPtr drawList, WalkabilitySnapshot terrain, + Vector2 playerGridPos, Vector2 minimapOrigin, float minimapSize) + { + var scaleX = minimapSize / terrain.Width; + var scaleY = minimapSize / terrain.Height; + var scale = Math.Min(scaleX, scaleY); + + // Background + drawList.AddRectFilled(minimapOrigin, + minimapOrigin + new Vector2(terrain.Width * scale, terrain.Height * scale), + 0xFF0A0A15); + + // Draw walkable cells (sampled) + var step = Math.Max(1, terrain.Width / 200); + for (var gy = 0; gy < terrain.Height; gy += step) + for (var gx = 0; gx < terrain.Width; gx += step) + { + if (!terrain.IsWalkable(gx, gy)) continue; + var px = minimapOrigin.X + gx * scale; + var py = minimapOrigin.Y + gy * scale; + drawList.AddRectFilled( + new Vector2(px, py), + new Vector2(px + scale * step, py + scale * step), + 0xFF2A2A3F); + } + + // Player dot + var playerPx = minimapOrigin + playerGridPos * scale; + drawList.AddCircleFilled(playerPx, 3f, 0xFF00FF00); + + // Border + drawList.AddRect(minimapOrigin, + minimapOrigin + new Vector2(terrain.Width * scale, terrain.Height * scale), + 0xFF666666); + } +} diff --git a/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs b/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs new file mode 100644 index 0000000..85eaf71 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs @@ -0,0 +1,482 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using ImGuiNET; +using Veldrid; + +namespace Nexus.Simulator.Rendering; + +/// +/// Minimal ImGui renderer for Veldrid 4.9.0 on D3D11. +/// Based on the canonical veldrid-imgui integration. +/// +public sealed class VeldridImGuiRenderer : IDisposable +{ + private readonly GraphicsDevice _gd; + + // GPU resources + private DeviceBuffer _vertexBuffer = null!; + private DeviceBuffer _indexBuffer = null!; + private DeviceBuffer _projMatrixBuffer = null!; + private Texture _fontTexture = null!; + private TextureView _fontTextureView = null!; + private ResourceSet _fontResourceSet = null!; + private ResourceLayout _layout = null!; + private ResourceLayout _textureLayout = null!; + private Pipeline _pipeline = null!; + private Shader[] _shaders = null!; + + private int _windowWidth; + private int _windowHeight; + private readonly IntPtr _fontAtlasId = (IntPtr)1; + + // Per-texture resource sets + private readonly Dictionary _resourceSets = new(); + + public VeldridImGuiRenderer(GraphicsDevice gd, OutputDescription outputDescription, int width, int height) + { + _gd = gd; + _windowWidth = width; + _windowHeight = height; + + var context = ImGui.CreateContext(); + ImGui.SetCurrentContext(context); + + var io = ImGui.GetIO(); + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; + io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard; + io.DisplaySize = new Vector2(width, height); + io.DisplayFramebufferScale = Vector2.One; + + CreateDeviceResources(gd, outputDescription); + SetupKeyMappings(); + } + + public void WindowResized(int width, int height) + { + _windowWidth = width; + _windowHeight = height; + ImGui.GetIO().DisplaySize = new Vector2(width, height); + } + + public void Update(float deltaSeconds, InputSnapshot snapshot) + { + var io = ImGui.GetIO(); + io.DisplaySize = new Vector2(_windowWidth, _windowHeight); + io.DeltaTime = deltaSeconds > 0 ? deltaSeconds : 1f / 60f; + + UpdateInput(snapshot); + + ImGui.NewFrame(); + } + + public void Render(GraphicsDevice gd, CommandList cl) + { + ImGui.Render(); + RenderImDrawData(ImGui.GetDrawData(), gd, cl); + } + + private void CreateDeviceResources(GraphicsDevice gd, OutputDescription outputDescription) + { + var factory = gd.ResourceFactory; + + _vertexBuffer = factory.CreateBuffer(new BufferDescription(10000, BufferUsage.VertexBuffer | BufferUsage.Dynamic)); + _indexBuffer = factory.CreateBuffer(new BufferDescription(2000, BufferUsage.IndexBuffer | BufferUsage.Dynamic)); + _projMatrixBuffer = factory.CreateBuffer(new BufferDescription(64, BufferUsage.UniformBuffer | BufferUsage.Dynamic)); + + // Create shaders using HLSL + _shaders = CreateShaders(gd); + + // Font texture + RecreateFontDeviceTexture(gd); + + // Resource layouts + _layout = factory.CreateResourceLayout(new ResourceLayoutDescription( + new ResourceLayoutElementDescription("ProjectionMatrixBuffer", ResourceKind.UniformBuffer, ShaderStages.Vertex), + new ResourceLayoutElementDescription("MainSampler", ResourceKind.Sampler, ShaderStages.Fragment))); + + _textureLayout = factory.CreateResourceLayout(new ResourceLayoutDescription( + new ResourceLayoutElementDescription("MainTexture", ResourceKind.TextureReadOnly, ShaderStages.Fragment))); + + // Pipeline + var vertexLayout = new VertexLayoutDescription( + new VertexElementDescription("in_position", VertexElementSemantic.TextureCoordinate, VertexElementFormat.Float2), + new VertexElementDescription("in_texCoord", VertexElementSemantic.TextureCoordinate, VertexElementFormat.Float2), + new VertexElementDescription("in_color", VertexElementSemantic.TextureCoordinate, VertexElementFormat.Byte4_Norm)); + + var pipelineDesc = new GraphicsPipelineDescription + { + BlendState = new BlendStateDescription + { + AttachmentStates = [ + new BlendAttachmentDescription + { + BlendEnabled = true, + SourceColorFactor = BlendFactor.SourceAlpha, + DestinationColorFactor = BlendFactor.InverseSourceAlpha, + ColorFunction = BlendFunction.Add, + SourceAlphaFactor = BlendFactor.One, + DestinationAlphaFactor = BlendFactor.InverseSourceAlpha, + AlphaFunction = BlendFunction.Add, + }, + ], + }, + DepthStencilState = new DepthStencilStateDescription(false, false, ComparisonKind.Always), + RasterizerState = new RasterizerStateDescription( + FaceCullMode.None, PolygonFillMode.Solid, FrontFace.Clockwise, + true, true), + PrimitiveTopology = PrimitiveTopology.TriangleList, + ResourceLayouts = [_layout, _textureLayout], + ShaderSet = new ShaderSetDescription([vertexLayout], _shaders), + Outputs = outputDescription, + }; + + _pipeline = factory.CreateGraphicsPipeline(ref pipelineDesc); + } + + private void RecreateFontDeviceTexture(GraphicsDevice gd) + { + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height, out int bytesPerPixel); + + _fontTexture?.Dispose(); + _fontTextureView?.Dispose(); + + _fontTexture = gd.ResourceFactory.CreateTexture(TextureDescription.Texture2D( + (uint)width, (uint)height, 1, 1, + PixelFormat.R8_G8_B8_A8_UNorm, TextureUsage.Sampled)); + _fontTexture.Name = "ImGui.NET Font Texture"; + + gd.UpdateTexture(_fontTexture, pixels, (uint)(bytesPerPixel * width * height), 0, 0, 0, + (uint)width, (uint)height, 1, 0, 0); + + _fontTextureView = gd.ResourceFactory.CreateTextureView(_fontTexture); + + io.Fonts.SetTexID(_fontAtlasId); + io.Fonts.ClearTexData(); + + // Create resource set for font + if (_textureLayout is not null) + { + _fontResourceSet?.Dispose(); + _fontResourceSet = gd.ResourceFactory.CreateResourceSet(new ResourceSetDescription( + _textureLayout, _fontTextureView)); + _resourceSets[_fontAtlasId] = _fontResourceSet; + } + } + + private Shader[] CreateShaders(GraphicsDevice gd) + { + // For D3D11, use HLSL compiled at runtime + // For other backends, we'd need SPIRV + var backend = gd.BackendType; + + byte[] vertexShaderBytes; + byte[] fragmentShaderBytes; + + if (backend == GraphicsBackend.Direct3D11) + { + vertexShaderBytes = CompileHlsl(VertexShaderHlsl, "main", "vs_5_0"); + fragmentShaderBytes = CompileHlsl(FragmentShaderHlsl, "main", "ps_5_0"); + } + else + { + throw new NotSupportedException($"Backend {backend} not supported. Use Direct3D11."); + } + + var vertexShader = gd.ResourceFactory.CreateShader(new ShaderDescription( + ShaderStages.Vertex, vertexShaderBytes, "main")); + var fragmentShader = gd.ResourceFactory.CreateShader(new ShaderDescription( + ShaderStages.Fragment, fragmentShaderBytes, "main")); + + return [vertexShader, fragmentShader]; + } + + private void RenderImDrawData(ImDrawDataPtr drawData, GraphicsDevice gd, CommandList cl) + { + if (drawData.CmdListsCount == 0) return; + + // Resize vertex/index buffers if needed + var totalVtxSize = (uint)(drawData.TotalVtxCount * Unsafe.SizeOf()); + var totalIdxSize = (uint)(drawData.TotalIdxCount * sizeof(ushort)); + + if (totalVtxSize > _vertexBuffer.SizeInBytes) + { + _vertexBuffer.Dispose(); + _vertexBuffer = gd.ResourceFactory.CreateBuffer(new BufferDescription( + totalVtxSize * 2, BufferUsage.VertexBuffer | BufferUsage.Dynamic)); + } + + if (totalIdxSize > _indexBuffer.SizeInBytes) + { + _indexBuffer.Dispose(); + _indexBuffer = gd.ResourceFactory.CreateBuffer(new BufferDescription( + totalIdxSize * 2, BufferUsage.IndexBuffer | BufferUsage.Dynamic)); + } + + // Upload vertex/index data + uint vtxOffset = 0, idxOffset = 0; + for (var i = 0; i < drawData.CmdListsCount; i++) + { + var cmdList = drawData.CmdLists[i]; + cl.UpdateBuffer(_vertexBuffer, vtxOffset, + cmdList.VtxBuffer.Data, (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf())); + cl.UpdateBuffer(_indexBuffer, idxOffset, + cmdList.IdxBuffer.Data, (uint)(cmdList.IdxBuffer.Size * sizeof(ushort))); + vtxOffset += (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf()); + idxOffset += (uint)(cmdList.IdxBuffer.Size * sizeof(ushort)); + } + + // Update projection matrix + var io = ImGui.GetIO(); + var mvp = Matrix4x4.CreateOrthographicOffCenter( + drawData.DisplayPos.X, + drawData.DisplayPos.X + drawData.DisplaySize.X, + drawData.DisplayPos.Y + drawData.DisplaySize.Y, + drawData.DisplayPos.Y, + -1f, 1f); + cl.UpdateBuffer(_projMatrixBuffer, 0, ref mvp); + + cl.SetVertexBuffer(0, _vertexBuffer); + cl.SetIndexBuffer(_indexBuffer, IndexFormat.UInt16); + cl.SetPipeline(_pipeline); + + // Create main resource set + var mainResourceSet = gd.ResourceFactory.CreateResourceSet(new ResourceSetDescription( + _layout, _projMatrixBuffer, gd.PointSampler)); + cl.SetGraphicsResourceSet(0, mainResourceSet); + + // Draw + var clipOff = drawData.DisplayPos; + vtxOffset = 0; + idxOffset = 0; + for (var n = 0; n < drawData.CmdListsCount; n++) + { + var cmdList = drawData.CmdLists[n]; + for (var cmdI = 0; cmdI < cmdList.CmdBuffer.Size; cmdI++) + { + var pcmd = cmdList.CmdBuffer[cmdI]; + + if (pcmd.TextureId != IntPtr.Zero) + { + if (_resourceSets.TryGetValue(pcmd.TextureId, out var rs)) + cl.SetGraphicsResourceSet(1, rs); + } + + var clipRect = pcmd.ClipRect; + cl.SetScissorRect(0, + (uint)(clipRect.X - clipOff.X), + (uint)(clipRect.Y - clipOff.Y), + (uint)(clipRect.Z - clipRect.X), + (uint)(clipRect.W - clipRect.Y)); + + cl.DrawIndexed(pcmd.ElemCount, 1, + pcmd.IdxOffset + idxOffset, + (int)(pcmd.VtxOffset + vtxOffset), + 0); + } + vtxOffset += (uint)cmdList.VtxBuffer.Size; + idxOffset += (uint)cmdList.IdxBuffer.Size; + } + + mainResourceSet.Dispose(); + } + + private void UpdateInput(InputSnapshot snapshot) + { + var io = ImGui.GetIO(); + + io.MousePos = snapshot.MousePosition; + + io.MouseDown[0] = snapshot.IsMouseDown(MouseButton.Left); + io.MouseDown[1] = snapshot.IsMouseDown(MouseButton.Right); + io.MouseDown[2] = snapshot.IsMouseDown(MouseButton.Middle); + io.MouseWheel = snapshot.WheelDelta; + + foreach (var ke in snapshot.KeyEvents) + { + var imguiKey = ConvertKey(ke.Key); + if (imguiKey != ImGuiKey.None) + io.AddKeyEvent(imguiKey, ke.Down); + } + + foreach (var c in snapshot.KeyCharPresses) + io.AddInputCharacter(c); + } + + private static ImGuiKey ConvertKey(Key key) => key switch + { + Key.Tab => ImGuiKey.Tab, + Key.Left => ImGuiKey.LeftArrow, + Key.Right => ImGuiKey.RightArrow, + Key.Up => ImGuiKey.UpArrow, + Key.Down => ImGuiKey.DownArrow, + Key.Home => ImGuiKey.Home, + Key.End => ImGuiKey.End, + Key.Delete => ImGuiKey.Delete, + Key.BackSpace => ImGuiKey.Backspace, + Key.Enter => ImGuiKey.Enter, + Key.Escape => ImGuiKey.Escape, + Key.Space => ImGuiKey.Space, + Key.A => ImGuiKey.A, + Key.C => ImGuiKey.C, + Key.V => ImGuiKey.V, + Key.X => ImGuiKey.X, + Key.Y => ImGuiKey.Y, + Key.Z => ImGuiKey.Z, + _ => ImGuiKey.None, + }; + + private void SetupKeyMappings() + { + // ImGui.NET 1.91+ uses the key event API directly, no need for explicit mappings + } + + // ── HLSL Shader Sources ── + + private const string VertexShaderHlsl = @" +cbuffer ProjectionMatrixBuffer : register(b0) +{ + float4x4 ProjectionMatrix; +}; + +struct VS_INPUT +{ + float2 pos : TEXCOORD0; + float2 uv : TEXCOORD1; + float4 col : TEXCOORD2; +}; + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float4 col : COLOR0; + float2 uv : TEXCOORD0; +}; + +PS_INPUT main(VS_INPUT input) +{ + PS_INPUT output; + output.pos = mul(ProjectionMatrix, float4(input.pos.xy, 0.0, 1.0)); + output.col = input.col; + output.uv = input.uv; + return output; +} +"; + + private const string FragmentShaderHlsl = @" +Texture2D MainTexture : register(t0); +SamplerState MainSampler : register(s0); + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float4 col : COLOR0; + float2 uv : TEXCOORD0; +}; + +float4 main(PS_INPUT input) : SV_Target +{ + float4 texColor = MainTexture.Sample(MainSampler, input.uv); + return input.col * texColor; +} +"; + + // ── D3DCompiler P/Invoke ── + + [DllImport("d3dcompiler_47.dll", CallingConvention = CallingConvention.StdCall)] + private static extern int D3DCompile( + [MarshalAs(UnmanagedType.LPStr)] string pSrcData, + int srcDataSize, + [MarshalAs(UnmanagedType.LPStr)] string? pSourceName, + IntPtr pDefines, + IntPtr pInclude, + [MarshalAs(UnmanagedType.LPStr)] string pEntrypoint, + [MarshalAs(UnmanagedType.LPStr)] string pTarget, + uint flags1, + uint flags2, + out IntPtr ppCode, + out IntPtr ppErrorMsgs); + + [DllImport("d3dcompiler_47.dll", CallingConvention = CallingConvention.StdCall)] + private static extern IntPtr D3DGetBlobPart(IntPtr pSrcData, int srcDataSize, int part, uint flags, out IntPtr ppPart); + + // ID3DBlob vtable offsets + private static IntPtr BlobGetBufferPointer(IntPtr blob) + { + var vtable = Marshal.ReadIntPtr(blob); + var getBufferPtr = Marshal.ReadIntPtr(vtable, 3 * IntPtr.Size); // IUnknown (3 methods) + GetBufferPointer + var del = Marshal.GetDelegateForFunctionPointer(getBufferPtr); + return del(blob); + } + + private static int BlobGetBufferSize(IntPtr blob) + { + var vtable = Marshal.ReadIntPtr(blob); + var getBufferSize = Marshal.ReadIntPtr(vtable, 4 * IntPtr.Size); // IUnknown (3 methods) + GetBufferPointer + GetBufferSize + var del = Marshal.GetDelegateForFunctionPointer(getBufferSize); + return del(blob); + } + + private static void BlobRelease(IntPtr blob) + { + var vtable = Marshal.ReadIntPtr(blob); + var release = Marshal.ReadIntPtr(vtable, 2 * IntPtr.Size); // IUnknown::Release + var del = Marshal.GetDelegateForFunctionPointer(release); + del(blob); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate IntPtr GetBufferPointerDelegate(IntPtr self); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int GetBufferSizeDelegate(IntPtr self); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int ReleaseDelegate(IntPtr self); + + private static byte[] CompileHlsl(string source, string entryPoint, string target) + { + var hr = D3DCompile(source, source.Length, null, IntPtr.Zero, IntPtr.Zero, + entryPoint, target, 0, 0, out var codeBlob, out var errorBlob); + + if (hr != 0 || codeBlob == IntPtr.Zero) + { + var error = "Unknown shader compilation error"; + if (errorBlob != IntPtr.Zero) + { + var errorPtr = BlobGetBufferPointer(errorBlob); + error = Marshal.PtrToStringAnsi(errorPtr) ?? error; + BlobRelease(errorBlob); + } + throw new InvalidOperationException($"HLSL compilation failed: {error}"); + } + + if (errorBlob != IntPtr.Zero) + BlobRelease(errorBlob); + + var bufferPtr = BlobGetBufferPointer(codeBlob); + var bufferSize = BlobGetBufferSize(codeBlob); + var result = new byte[bufferSize]; + Marshal.Copy(bufferPtr, result, 0, bufferSize); + BlobRelease(codeBlob); + return result; + } + + public void Dispose() + { + _vertexBuffer?.Dispose(); + _indexBuffer?.Dispose(); + _projMatrixBuffer?.Dispose(); + _fontTexture?.Dispose(); + _fontTextureView?.Dispose(); + _fontResourceSet?.Dispose(); + _pipeline?.Dispose(); + _layout?.Dispose(); + _textureLayout?.Dispose(); + foreach (var shader in _shaders) + shader?.Dispose(); + foreach (var rs in _resourceSets.Values) + rs?.Dispose(); + } +} diff --git a/src/Nexus.Simulator/World/SimEnemy.cs b/src/Nexus.Simulator/World/SimEnemy.cs new file mode 100644 index 0000000..95903b4 --- /dev/null +++ b/src/Nexus.Simulator/World/SimEnemy.cs @@ -0,0 +1,76 @@ +using System.Numerics; +using Nexus.Core; + +namespace Nexus.Simulator.World; + +public enum EnemyAiState +{ + Idle, + Chasing, + Attacking, + Dead, +} + +public class SimEnemy +{ + private static uint _nextId = 1000; + + public uint Id { get; } + public Vector2 Position { get; set; } + public int Health { get; set; } + public int MaxHealth { get; set; } + public MonsterRarity Rarity { get; set; } + public EnemyAiState AiState { get; set; } = EnemyAiState.Idle; + public float MoveSpeed { get; set; } + + // Timers + public float AttackCooldownRemaining { get; set; } + public float DespawnTimer { get; set; } + public float RespawnTimer { get; set; } + + // Wander + public Vector2 WanderTarget { get; set; } + public float WanderTimer { get; set; } + public Vector2 SpawnPosition { get; set; } + + public bool IsAlive => Health > 0; + public bool IsAttacking => AiState == EnemyAiState.Attacking; + + public SimEnemy(Vector2 position, MonsterRarity rarity, int baseHealth, float moveSpeed) + { + Id = Interlocked.Increment(ref _nextId); + Position = position; + SpawnPosition = position; + Rarity = rarity; + MoveSpeed = moveSpeed; + + var hpMultiplier = rarity switch + { + MonsterRarity.Magic => 1.5f, + MonsterRarity.Rare => 3f, + MonsterRarity.Unique => 5f, + _ => 1f, + }; + MaxHealth = (int)(baseHealth * hpMultiplier); + Health = MaxHealth; + } + + public void TakeDamage(int damage) + { + if (!IsAlive) return; + Health = Math.Max(0, Health - damage); + if (Health <= 0) + { + AiState = EnemyAiState.Dead; + DespawnTimer = 2f; + } + } + + public MonsterThreatLevel GetThreatLevel() => Rarity switch + { + MonsterRarity.Magic => MonsterThreatLevel.Magic, + MonsterRarity.Rare => MonsterThreatLevel.Rare, + MonsterRarity.Unique => MonsterThreatLevel.Unique, + _ => MonsterThreatLevel.Normal, + }; +} diff --git a/src/Nexus.Simulator/World/SimPlayer.cs b/src/Nexus.Simulator/World/SimPlayer.cs new file mode 100644 index 0000000..1ca20ab --- /dev/null +++ b/src/Nexus.Simulator/World/SimPlayer.cs @@ -0,0 +1,58 @@ +using System.Numerics; + +namespace Nexus.Simulator.World; + +public class SimPlayer +{ + public Vector2 Position { get; set; } + public int Health { get; set; } + public int MaxHealth { get; set; } + public int Mana { get; set; } + public int MaxMana { get; set; } + public float MoveSpeed { get; set; } + public float HealthRegen { get; set; } + public float ManaRegen { get; set; } + + // Accumulate fractional regen + private float _healthRegenAccum; + private float _manaRegenAccum; + + public SimPlayer(int maxHealth, int maxMana, float moveSpeed, float healthRegen, float manaRegen) + { + MaxHealth = maxHealth; + MaxMana = maxMana; + Health = maxHealth; + Mana = maxMana; + MoveSpeed = moveSpeed; + HealthRegen = healthRegen; + ManaRegen = manaRegen; + } + + public void Update(float dt) + { + // Regenerate + _healthRegenAccum += HealthRegen * dt; + _manaRegenAccum += ManaRegen * dt; + + if (_healthRegenAccum >= 1f) + { + var amount = (int)_healthRegenAccum; + Health = Math.Min(MaxHealth, Health + amount); + _healthRegenAccum -= amount; + } + + if (_manaRegenAccum >= 1f) + { + var amount = (int)_manaRegenAccum; + Mana = Math.Min(MaxMana, Mana + amount); + _manaRegenAccum -= amount; + } + } + + public void TakeDamage(int damage) + { + Health = Math.Max(0, Health - damage); + } + + public bool IsAlive => Health > 0; +} diff --git a/src/Nexus.Simulator/World/SimProjectile.cs b/src/Nexus.Simulator/World/SimProjectile.cs new file mode 100644 index 0000000..b4199b0 --- /dev/null +++ b/src/Nexus.Simulator/World/SimProjectile.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +namespace Nexus.Simulator.World; + +public class SimProjectile +{ + public Vector2 Position { get; set; } + public Vector2 Direction { get; set; } + public float Speed { get; set; } + public float MaxRange { get; set; } + public float HitRadius { get; set; } + public int Damage { get; set; } + public float DistanceTraveled { get; set; } + public bool IsExpired { get; set; } + + public SimProjectile(Vector2 origin, Vector2 direction, float speed, float maxRange, float hitRadius, int damage) + { + Position = origin; + Direction = Vector2.Normalize(direction); + Speed = speed; + MaxRange = maxRange; + HitRadius = hitRadius; + Damage = damage; + } + + public void Update(float dt) + { + var step = Speed * dt; + Position += Direction * step; + DistanceTraveled += step; + if (DistanceTraveled >= MaxRange) + IsExpired = true; + } +} diff --git a/src/Nexus.Simulator/World/SimSkillEffect.cs b/src/Nexus.Simulator/World/SimSkillEffect.cs new file mode 100644 index 0000000..073a51f --- /dev/null +++ b/src/Nexus.Simulator/World/SimSkillEffect.cs @@ -0,0 +1,26 @@ +using System.Numerics; + +namespace Nexus.Simulator.World; + +public enum SkillEffectType +{ + Melee, + Aoe, + Projectile, +} + +public class SimSkillEffect +{ + public SkillEffectType Type { get; set; } + public Vector2 Origin { get; set; } + public Vector2 TargetPosition { get; set; } + public float Radius { get; set; } + public float ConeAngle { get; set; } + public int Damage { get; set; } + public float Duration { get; set; } = 0.3f; + public float Elapsed { get; set; } + public bool Applied { get; set; } + + public bool IsExpired => Elapsed >= Duration; + public float Progress => Duration > 0 ? Elapsed / Duration : 1f; +} diff --git a/src/Nexus.Simulator/World/SimWorld.cs b/src/Nexus.Simulator/World/SimWorld.cs new file mode 100644 index 0000000..a35796d --- /dev/null +++ b/src/Nexus.Simulator/World/SimWorld.cs @@ -0,0 +1,432 @@ +using System.Numerics; +using Nexus.Core; +using Nexus.Simulator.Config; + +namespace Nexus.Simulator.World; + +public class SimWorld +{ + private readonly SimConfig _config; + private readonly Random _rng = new(); + + public SimPlayer Player { get; } + public List Enemies { get; } = []; + public List Projectiles { get; } = []; + public List ActiveEffects { get; } = []; + public WalkabilitySnapshot Terrain { get; private set; } + public long TickNumber { get; private set; } + + // Pending respawns + private readonly List<(float timer, MonsterRarity rarity)> _respawnQueue = []; + + // Queued actions from SimInputController + public Vector2 MoveDirection { get; set; } + public Vector2 MouseWorldPos { get; set; } + private readonly Queue<(ushort scanCode, Vector2 targetWorldPos)> _skillQueue = new(); + + public SimWorld(SimConfig config) + { + _config = config; + Terrain = TerrainGenerator.Generate(config.TerrainWidth, config.TerrainHeight); + + // Spawn player at center + var gridToWorld = 1f / config.WorldToGrid; + var (sx, sy) = TerrainGenerator.FindSpawnPosition(Terrain); + Player = new SimPlayer( + config.PlayerMaxHealth, config.PlayerMaxMana, + config.PlayerMoveSpeed, config.PlayerHealthRegen, config.PlayerManaRegen) + { + Position = new Vector2(sx * gridToWorld, sy * gridToWorld), + }; + + // Spawn initial enemies + SpawnEnemies(config.TargetEnemyCount); + } + + public void RegenerateTerrain() + { + Terrain = TerrainGenerator.Generate(_config.TerrainWidth, _config.TerrainHeight); + var gridToWorld = 1f / _config.WorldToGrid; + var (sx, sy) = TerrainGenerator.FindSpawnPosition(Terrain); + Player.Position = new Vector2(sx * gridToWorld, sy * gridToWorld); + Player.Health = Player.MaxHealth; + Player.Mana = Player.MaxMana; + Enemies.Clear(); + Projectiles.Clear(); + ActiveEffects.Clear(); + _respawnQueue.Clear(); + SpawnEnemies(_config.TargetEnemyCount); + } + + public void QueueSkill(ushort scanCode, Vector2 targetWorldPos) + { + _skillQueue.Enqueue((scanCode, targetWorldPos)); + } + + public void Tick(float dt) + { + if (_config.IsPaused) return; + + dt *= _config.SpeedMultiplier; + TickNumber++; + + // 1. Move player + MovePlayer(dt); + + // 2. Process queued skills + ProcessSkills(); + + // 3. Update projectiles + UpdateProjectiles(dt); + + // 4. Update skill effects + UpdateEffects(dt); + + // 5. Update enemy AI + UpdateEnemies(dt); + + // 6. Process respawn queue + UpdateRespawns(dt); + + // 7. Player regen + Player.Update(dt); + } + + private void MovePlayer(float dt) + { + if (MoveDirection.LengthSquared() < 0.001f) return; + + var dir = Vector2.Normalize(MoveDirection); + var newPos = Player.Position + dir * Player.MoveSpeed * dt; + + // Terrain collision + var gx = (int)(newPos.X * _config.WorldToGrid); + var gy = (int)(newPos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + Player.Position = newPos; + else + { + // Try sliding along X + var slideX = new Vector2(Player.Position.X + dir.X * Player.MoveSpeed * dt, Player.Position.Y); + var sgx = (int)(slideX.X * _config.WorldToGrid); + var sgy = (int)(slideX.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(sgx, sgy)) + { + Player.Position = slideX; + return; + } + + // Try sliding along Y + var slideY = new Vector2(Player.Position.X, Player.Position.Y + dir.Y * Player.MoveSpeed * dt); + sgx = (int)(slideY.X * _config.WorldToGrid); + sgy = (int)(slideY.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(sgx, sgy)) + Player.Position = slideY; + } + } + + private void ProcessSkills() + { + while (_skillQueue.TryDequeue(out var skill)) + { + // Determine skill type based on scan code slot + // Slots 0-1 (LMB/RMB) = melee, 3 (Q) = AOE, 4 (E) = projectile, else = melee + var type = GetSkillType(skill.scanCode); + var targetPos = skill.targetWorldPos; + + switch (type) + { + case SkillEffectType.Melee: + ProcessMeleeSkill(targetPos); + break; + case SkillEffectType.Aoe: + ProcessAoeSkill(targetPos); + break; + case SkillEffectType.Projectile: + ProcessProjectileSkill(targetPos); + break; + } + } + } + + private SkillEffectType GetSkillType(ushort scanCode) + { + return scanCode switch + { + 0x10 => SkillEffectType.Aoe, // Q + 0x12 => SkillEffectType.Projectile, // E + 0x13 => SkillEffectType.Aoe, // R + 0x14 => SkillEffectType.Projectile, // T + _ => SkillEffectType.Melee, + }; + } + + private void ProcessMeleeSkill(Vector2 targetPos) + { + var dir = targetPos - Player.Position; + if (dir.LengthSquared() > 0) + dir = Vector2.Normalize(dir); + else + dir = Vector2.UnitX; + + var effect = new SimSkillEffect + { + Type = SkillEffectType.Melee, + Origin = Player.Position, + TargetPosition = targetPos, + Radius = _config.MeleeRange, + ConeAngle = _config.MeleeConeAngle, + Damage = _config.SkillBaseDamage, + }; + ActiveEffects.Add(effect); + + // Apply melee damage immediately + var halfAngle = _config.MeleeConeAngle * MathF.PI / 360f; + foreach (var enemy in Enemies) + { + if (!enemy.IsAlive) continue; + var toEnemy = enemy.Position - Player.Position; + var dist = toEnemy.Length(); + if (dist > _config.MeleeRange) continue; + + if (dist > 0) + { + var angle = MathF.Acos(Vector2.Dot(Vector2.Normalize(toEnemy), dir)); + if (angle > halfAngle) continue; + } + + enemy.TakeDamage(_config.SkillBaseDamage); + } + } + + private void ProcessAoeSkill(Vector2 targetPos) + { + var effect = new SimSkillEffect + { + Type = SkillEffectType.Aoe, + Origin = Player.Position, + TargetPosition = targetPos, + Radius = _config.AoeRadius, + Damage = _config.SkillBaseDamage, + }; + ActiveEffects.Add(effect); + + // Apply AOE damage + foreach (var enemy in Enemies) + { + if (!enemy.IsAlive) continue; + if (Vector2.Distance(enemy.Position, targetPos) <= _config.AoeRadius) + enemy.TakeDamage(_config.SkillBaseDamage); + } + } + + private void ProcessProjectileSkill(Vector2 targetPos) + { + var dir = targetPos - Player.Position; + if (dir.LengthSquared() < 1f) dir = Vector2.UnitX; + + var projectile = new SimProjectile( + Player.Position, + dir, + _config.ProjectileSpeed, + _config.ProjectileRange, + _config.ProjectileHitRadius, + _config.SkillBaseDamage); + + Projectiles.Add(projectile); + + ActiveEffects.Add(new SimSkillEffect + { + Type = SkillEffectType.Projectile, + Origin = Player.Position, + TargetPosition = targetPos, + Radius = _config.ProjectileHitRadius, + Damage = _config.SkillBaseDamage, + Duration = _config.ProjectileRange / _config.ProjectileSpeed, + }); + } + + private void UpdateProjectiles(float dt) + { + for (var i = Projectiles.Count - 1; i >= 0; i--) + { + var proj = Projectiles[i]; + proj.Update(dt); + + // Check terrain collision + var gx = (int)(proj.Position.X * _config.WorldToGrid); + var gy = (int)(proj.Position.Y * _config.WorldToGrid); + if (!Terrain.IsWalkable(gx, gy)) + { + proj.IsExpired = true; + } + + // Check enemy hits + if (!proj.IsExpired) + { + foreach (var enemy in Enemies) + { + if (!enemy.IsAlive) continue; + if (Vector2.Distance(enemy.Position, proj.Position) <= proj.HitRadius) + { + enemy.TakeDamage(proj.Damage); + proj.IsExpired = true; + break; + } + } + } + + if (proj.IsExpired) + Projectiles.RemoveAt(i); + } + } + + private void UpdateEffects(float dt) + { + for (var i = ActiveEffects.Count - 1; i >= 0; i--) + { + ActiveEffects[i].Elapsed += dt; + if (ActiveEffects[i].IsExpired) + ActiveEffects.RemoveAt(i); + } + } + + private void UpdateEnemies(float dt) + { + for (var i = Enemies.Count - 1; i >= 0; i--) + { + var enemy = Enemies[i]; + + if (enemy.AiState == EnemyAiState.Dead) + { + enemy.DespawnTimer -= dt; + if (enemy.DespawnTimer <= 0) + { + // Queue respawn + _respawnQueue.Add((_config.EnemyRespawnTime, enemy.Rarity)); + Enemies.RemoveAt(i); + } + continue; + } + + // Attack cooldown + if (enemy.AttackCooldownRemaining > 0) + enemy.AttackCooldownRemaining -= dt; + + var dist = Vector2.Distance(enemy.Position, Player.Position); + + if (dist <= _config.EnemyAttackRange && Player.IsAlive) + { + // In attack range + enemy.AiState = EnemyAiState.Attacking; + if (enemy.AttackCooldownRemaining <= 0) + { + Player.TakeDamage(_config.EnemyAttackDamage); + enemy.AttackCooldownRemaining = _config.EnemyAttackCooldown; + } + } + else if (dist <= _config.EnemyAggroRange && Player.IsAlive) + { + // Chase player + enemy.AiState = EnemyAiState.Chasing; + var dir = Vector2.Normalize(Player.Position - enemy.Position); + var newPos = enemy.Position + dir * enemy.MoveSpeed * dt; + + var gx = (int)(newPos.X * _config.WorldToGrid); + var gy = (int)(newPos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + enemy.Position = newPos; + } + else + { + // Idle: random wander + enemy.AiState = EnemyAiState.Idle; + enemy.WanderTimer -= dt; + if (enemy.WanderTimer <= 0) + { + // Pick new wander target + var angle = _rng.NextSingle() * MathF.Tau; + var dist2 = _rng.NextSingle() * _config.EnemyWanderRadius; + enemy.WanderTarget = enemy.SpawnPosition + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist2; + enemy.WanderTimer = 2f + _rng.NextSingle() * 3f; + } + + if (Vector2.Distance(enemy.Position, enemy.WanderTarget) > 10f) + { + var dir = Vector2.Normalize(enemy.WanderTarget - enemy.Position); + var newPos = enemy.Position + dir * enemy.MoveSpeed * 0.3f * dt; + var gx = (int)(newPos.X * _config.WorldToGrid); + var gy = (int)(newPos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + enemy.Position = newPos; + } + } + } + } + + private void UpdateRespawns(float dt) + { + for (var i = _respawnQueue.Count - 1; i >= 0; i--) + { + var (timer, rarity) = _respawnQueue[i]; + timer -= dt; + if (timer <= 0) + { + SpawnEnemy(rarity); + _respawnQueue.RemoveAt(i); + } + else + { + _respawnQueue[i] = (timer, rarity); + } + } + + // Maintain population + var aliveCount = Enemies.Count(e => e.IsAlive) + _respawnQueue.Count; + while (aliveCount < _config.TargetEnemyCount) + { + SpawnEnemy(RollRarity()); + aliveCount++; + } + } + + public void SpawnEnemyAt(Vector2 worldPos, MonsterRarity rarity) + { + var enemy = new SimEnemy(worldPos, rarity, _config.EnemyBaseHealth, + _config.PlayerMoveSpeed * _config.EnemyMoveSpeedFactor) + { + WanderTarget = worldPos, + WanderTimer = _rng.NextSingle() * 3f, + }; + Enemies.Add(enemy); + } + + private void SpawnEnemies(int count) + { + for (var i = 0; i < count; i++) + SpawnEnemy(RollRarity()); + } + + private void SpawnEnemy(MonsterRarity rarity) + { + var gridToWorld = 1f / _config.WorldToGrid; + var pos = TerrainGenerator.FindRandomWalkable(Terrain, _rng); + if (pos is null) return; + + var worldPos = new Vector2(pos.Value.x * gridToWorld, pos.Value.y * gridToWorld); + + // Don't spawn too close to player + if (Vector2.Distance(worldPos, Player.Position) < 300f) return; + + SpawnEnemyAt(worldPos, rarity); + } + + private MonsterRarity RollRarity() + { + var roll = _rng.NextSingle(); + if (roll < _config.UniqueChance) return MonsterRarity.Unique; + if (roll < _config.UniqueChance + _config.RareChance) return MonsterRarity.Rare; + if (roll < _config.UniqueChance + _config.RareChance + _config.MagicChance) return MonsterRarity.Magic; + return MonsterRarity.White; + } +} diff --git a/src/Nexus.Simulator/World/TerrainGenerator.cs b/src/Nexus.Simulator/World/TerrainGenerator.cs new file mode 100644 index 0000000..548f19c --- /dev/null +++ b/src/Nexus.Simulator/World/TerrainGenerator.cs @@ -0,0 +1,175 @@ +using Nexus.Core; + +namespace Nexus.Simulator.World; + +public static class TerrainGenerator +{ + private record Room(int X, int Y, int Width, int Height) + { + public int CenterX => X + Width / 2; + public int CenterY => Y + Height / 2; + } + + public static WalkabilitySnapshot Generate(int width, int height, int? seed = null) + { + var rng = seed.HasValue ? new Random(seed.Value) : new Random(); + var data = new byte[width * height]; // 0 = wall by default + + var rooms = new List(); + var attempts = 0; + var targetRooms = 15 + rng.Next(10); + + while (rooms.Count < targetRooms && attempts < 500) + { + attempts++; + var rw = rng.Next(20, 60); + var rh = rng.Next(20, 60); + var rx = rng.Next(2, width - rw - 2); + var ry = rng.Next(2, height - rh - 2); + + var candidate = new Room(rx, ry, rw, rh); + + // Check overlap with existing rooms (with margin) + var overlaps = false; + foreach (var existing in rooms) + { + if (candidate.X - 3 < existing.X + existing.Width && + candidate.X + candidate.Width + 3 > existing.X && + candidate.Y - 3 < existing.Y + existing.Height && + candidate.Y + candidate.Height + 3 > existing.Y) + { + overlaps = true; + break; + } + } + + if (!overlaps) + rooms.Add(candidate); + } + + // Carve rooms + foreach (var room in rooms) + { + for (var y = room.Y; y < room.Y + room.Height; y++) + for (var x = room.X; x < room.X + room.Width; x++) + data[y * width + x] = 1; + } + + // Connect rooms with corridors + for (var i = 1; i < rooms.Count; i++) + { + var a = rooms[i - 1]; + var b = rooms[i]; + CarveCorridorL(data, width, a.CenterX, a.CenterY, b.CenterX, b.CenterY, rng); + } + + // Also connect last to first for a loop + if (rooms.Count > 2) + { + var first = rooms[0]; + var last = rooms[^1]; + CarveCorridorL(data, width, first.CenterX, first.CenterY, last.CenterX, last.CenterY, rng); + } + + // Add some random extra connections + var extraConnections = rng.Next(3, 7); + for (var i = 0; i < extraConnections; i++) + { + var a = rooms[rng.Next(rooms.Count)]; + var b = rooms[rng.Next(rooms.Count)]; + if (a != b) + CarveCorridorL(data, width, a.CenterX, a.CenterY, b.CenterX, b.CenterY, rng); + } + + return new WalkabilitySnapshot + { + Width = width, + Height = height, + Data = data, + }; + } + + /// + /// Finds a walkable position near the center of the terrain. + /// + public static (int x, int y) FindSpawnPosition(WalkabilitySnapshot terrain) + { + var cx = terrain.Width / 2; + var cy = terrain.Height / 2; + + // Spiral outward + for (var r = 0; r < Math.Max(terrain.Width, terrain.Height); r++) + { + for (var dx = -r; dx <= r; dx++) + for (var dy = -r; dy <= r; dy++) + { + if (Math.Abs(dx) != r && Math.Abs(dy) != r) continue; + var x = cx + dx; + var y = cy + dy; + if (terrain.IsWalkable(x, y)) + return (x, y); + } + } + + return (cx, cy); + } + + /// + /// Finds a random walkable position. + /// + public static (int x, int y)? FindRandomWalkable(WalkabilitySnapshot terrain, Random rng, int maxAttempts = 200) + { + for (var i = 0; i < maxAttempts; i++) + { + var x = rng.Next(terrain.Width); + var y = rng.Next(terrain.Height); + if (terrain.IsWalkable(x, y)) + return (x, y); + } + return null; + } + + private static void CarveCorridorL(byte[] data, int width, int x1, int y1, int x2, int y2, Random rng) + { + var corridorWidth = 2 + rng.Next(2); + + // L-shaped: horizontal then vertical (or vice versa) + if (rng.Next(2) == 0) + { + CarveHorizontal(data, width, x1, x2, y1, corridorWidth); + CarveVertical(data, width, y1, y2, x2, corridorWidth); + } + else + { + CarveVertical(data, width, y1, y2, x1, corridorWidth); + CarveHorizontal(data, width, x1, x2, y2, corridorWidth); + } + } + + private static void CarveHorizontal(byte[] data, int width, int x1, int x2, int y, int thickness) + { + var xMin = Math.Min(x1, x2); + var xMax = Math.Max(x1, x2); + for (var x = xMin; x <= xMax; x++) + for (var dy = 0; dy < thickness; dy++) + { + var ry = y + dy; + if (ry >= 0 && ry < data.Length / width && x >= 0 && x < width) + data[ry * width + x] = 1; + } + } + + private static void CarveVertical(byte[] data, int width, int y1, int y2, int x, int thickness) + { + var height = data.Length / width; + var yMin = Math.Min(y1, y2); + var yMax = Math.Max(y1, y2); + for (var y = yMin; y <= yMax; y++) + for (var dx = 0; dx < thickness; dx++) + { + var rx = x + dx; + if (y >= 0 && y < height && rx >= 0 && rx < width) + data[y * width + rx] = 1; + } + } +} diff --git a/src/Roboto.Systems/CombatSystem.cs b/src/Nexus.Systems/CombatSystem.cs similarity index 99% rename from src/Roboto.Systems/CombatSystem.cs rename to src/Nexus.Systems/CombatSystem.cs index d7423ef..0309724 100644 --- a/src/Roboto.Systems/CombatSystem.cs +++ b/src/Nexus.Systems/CombatSystem.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Systems; +namespace Nexus.Systems; public class CombatSystem : ISystem { diff --git a/src/Roboto.Systems/LootSystem.cs b/src/Nexus.Systems/LootSystem.cs similarity index 86% rename from src/Roboto.Systems/LootSystem.cs rename to src/Nexus.Systems/LootSystem.cs index f9c366b..0d34b87 100644 --- a/src/Roboto.Systems/LootSystem.cs +++ b/src/Nexus.Systems/LootSystem.cs @@ -1,6 +1,6 @@ -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Systems; +namespace Nexus.Systems; public class LootSystem : ISystem { diff --git a/src/Roboto.Systems/MovementSystem.cs b/src/Nexus.Systems/MovementSystem.cs similarity index 97% rename from src/Roboto.Systems/MovementSystem.cs rename to src/Nexus.Systems/MovementSystem.cs index f19243d..bd46248 100644 --- a/src/Roboto.Systems/MovementSystem.cs +++ b/src/Nexus.Systems/MovementSystem.cs @@ -1,7 +1,7 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Systems; +namespace Nexus.Systems; /// /// Force-based avoidance: applies inverse-square repulsion from hostile monsters diff --git a/src/Roboto.Systems/NavigationSystem.cs b/src/Nexus.Systems/NavigationSystem.cs similarity index 85% rename from src/Roboto.Systems/NavigationSystem.cs rename to src/Nexus.Systems/NavigationSystem.cs index 31cc4fd..f611070 100644 --- a/src/Roboto.Systems/NavigationSystem.cs +++ b/src/Nexus.Systems/NavigationSystem.cs @@ -1,10 +1,10 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Systems; +namespace Nexus.Systems; /// -/// Simplified navigation system. Pathfinding has moved to Roboto.Navigation.PathFinder. +/// Simplified navigation system. Pathfinding has moved to Nexus.Pathfinding.PathFinder. /// This system just submits a MoveAction if an external direction is set. /// public class NavigationSystem : ISystem diff --git a/src/Roboto.Systems/Roboto.Systems.csproj b/src/Nexus.Systems/Nexus.Systems.csproj similarity index 83% rename from src/Roboto.Systems/Roboto.Systems.csproj rename to src/Nexus.Systems/Nexus.Systems.csproj index bcebb88..60b2b65 100644 --- a/src/Roboto.Systems/Roboto.Systems.csproj +++ b/src/Nexus.Systems/Nexus.Systems.csproj @@ -8,6 +8,6 @@ - + diff --git a/src/Roboto.Systems/ResourceSystem.cs b/src/Nexus.Systems/ResourceSystem.cs similarity index 97% rename from src/Roboto.Systems/ResourceSystem.cs rename to src/Nexus.Systems/ResourceSystem.cs index 44fadda..3205681 100644 --- a/src/Roboto.Systems/ResourceSystem.cs +++ b/src/Nexus.Systems/ResourceSystem.cs @@ -1,6 +1,6 @@ -using Roboto.Core; +using Nexus.Core; -namespace Roboto.Systems; +namespace Nexus.Systems; public class ResourceSystem : ISystem { diff --git a/src/Roboto.Systems/ThreatSystem.cs b/src/Nexus.Systems/ThreatSystem.cs similarity index 98% rename from src/Roboto.Systems/ThreatSystem.cs rename to src/Nexus.Systems/ThreatSystem.cs index 6dcc52a..bd44615 100644 --- a/src/Roboto.Systems/ThreatSystem.cs +++ b/src/Nexus.Systems/ThreatSystem.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Roboto.Core; +using Nexus.Core; using Serilog; -namespace Roboto.Systems; +namespace Nexus.Systems; /// /// Emergency threat response. Runs first (priority 50). diff --git a/src/Automata.Trade/ITradeMonitor.cs b/src/Nexus.Trade/ITradeMonitor.cs similarity index 93% rename from src/Automata.Trade/ITradeMonitor.cs rename to src/Nexus.Trade/ITradeMonitor.cs index ef45711..4c34cc6 100644 --- a/src/Automata.Trade/ITradeMonitor.cs +++ b/src/Nexus.Trade/ITradeMonitor.cs @@ -1,6 +1,6 @@ -using Automata.Core; +using Nexus.Core; -namespace Automata.Trade; +namespace Nexus.Trade; public interface ITradeMonitor : IAsyncDisposable { diff --git a/src/Automata.Trade/Automata.Trade.csproj b/src/Nexus.Trade/Nexus.Trade.csproj similarity index 77% rename from src/Automata.Trade/Automata.Trade.csproj rename to src/Nexus.Trade/Nexus.Trade.csproj index 0d0f761..3266cd4 100644 --- a/src/Automata.Trade/Automata.Trade.csproj +++ b/src/Nexus.Trade/Nexus.Trade.csproj @@ -5,6 +5,6 @@ enable - + diff --git a/src/Automata.Trade/Selectors.cs b/src/Nexus.Trade/Selectors.cs similarity index 97% rename from src/Automata.Trade/Selectors.cs rename to src/Nexus.Trade/Selectors.cs index eb0345a..c724ccd 100644 --- a/src/Automata.Trade/Selectors.cs +++ b/src/Nexus.Trade/Selectors.cs @@ -1,4 +1,4 @@ -namespace Automata.Trade; +namespace Nexus.Trade; public static class Selectors { diff --git a/src/Automata.Trade/TradeDaemonBridge.cs b/src/Nexus.Trade/TradeDaemonBridge.cs similarity index 99% rename from src/Automata.Trade/TradeDaemonBridge.cs rename to src/Nexus.Trade/TradeDaemonBridge.cs index df7c768..c51c163 100644 --- a/src/Automata.Trade/TradeDaemonBridge.cs +++ b/src/Nexus.Trade/TradeDaemonBridge.cs @@ -2,10 +2,10 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; -using Automata.Core; +using Nexus.Core; using Serilog; -namespace Automata.Trade; +namespace Nexus.Trade; public class TradeDaemonBridge : ITradeMonitor { diff --git a/src/Automata.Ui/App.axaml b/src/Nexus.Ui/App.axaml similarity index 92% rename from src/Automata.Ui/App.axaml rename to src/Nexus.Ui/App.axaml index a77fa7a..cc27e59 100644 --- a/src/Automata.Ui/App.axaml +++ b/src/Nexus.Ui/App.axaml @@ -1,7 +1,7 @@ diff --git a/src/Automata.Ui/App.axaml.cs b/src/Nexus.Ui/App.axaml.cs similarity index 89% rename from src/Automata.Ui/App.axaml.cs rename to src/Nexus.Ui/App.axaml.cs index 92a293f..822483d 100644 --- a/src/Automata.Ui/App.axaml.cs +++ b/src/Nexus.Ui/App.axaml.cs @@ -2,20 +2,20 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Microsoft.Extensions.DependencyInjection; -using Automata.Bot; -using Automata.Core; -using Automata.Game; -using Automata.GameLog; -using Automata.Inventory; -using Automata.Items; -using Automata.Screen; -using Automata.Screen.Ocr; -using Automata.Trade; -using Automata.Ui.Overlay; -using Automata.Ui.ViewModels; -using Automata.Ui.Views; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Game; +using Nexus.GameLog; +using Nexus.Inventory; +using Nexus.Items; +using Nexus.Screen; +using Nexus.Screen.Ocr; +using Nexus.Trade; +using Nexus.Ui.Overlay; +using Nexus.Ui.ViewModels; +using Nexus.Ui.Views; -namespace Automata.Ui; +namespace Nexus.Ui; public partial class App : Application { @@ -70,7 +70,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); var provider = services.BuildServiceProvider(); @@ -97,7 +97,7 @@ public partial class App : Application mainVm.AtlasVm = provider.GetRequiredService(); mainVm.CraftingVm = provider.GetRequiredService(); mainVm.MemoryVm = provider.GetRequiredService(); - mainVm.RobotoVm = provider.GetRequiredService(); + mainVm.NexusVm = provider.GetRequiredService(); mainVm.BrowserVm = provider.GetRequiredService(); var window = new MainWindow { DataContext = mainVm }; @@ -112,7 +112,7 @@ public partial class App : Application { overlay.Shutdown(); mainVm.Shutdown(); - mainVm.RobotoVm?.Shutdown(); + mainVm.NexusVm?.Shutdown(); await bot.DisposeAsync(); }; } diff --git a/src/Automata.Ui/Converters/ValueConverters.cs b/src/Nexus.Ui/Converters/ValueConverters.cs similarity index 98% rename from src/Automata.Ui/Converters/ValueConverters.cs rename to src/Nexus.Ui/Converters/ValueConverters.cs index 7550d71..fc43ef3 100644 --- a/src/Automata.Ui/Converters/ValueConverters.cs +++ b/src/Nexus.Ui/Converters/ValueConverters.cs @@ -2,10 +2,10 @@ using System.Globalization; using Avalonia; using Avalonia.Data.Converters; using Avalonia.Media; -using Automata.Core; -using Automata.Ui.ViewModels; +using Nexus.Core; +using Nexus.Ui.ViewModels; -namespace Automata.Ui.Converters; +namespace Nexus.Ui.Converters; public class LogLevelToBrushConverter : IValueConverter { diff --git a/src/Automata.Ui/Automata.Ui.csproj b/src/Nexus.Ui/Nexus.Ui.csproj similarity index 76% rename from src/Automata.Ui/Automata.Ui.csproj rename to src/Nexus.Ui/Nexus.Ui.csproj index ce866f4..f79c27c 100644 --- a/src/Automata.Ui/Automata.Ui.csproj +++ b/src/Nexus.Ui/Nexus.Ui.csproj @@ -16,16 +16,16 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/src/Automata.Ui/Overlay/D2dNativeMethods.cs b/src/Nexus.Ui/Overlay/D2dNativeMethods.cs similarity index 99% rename from src/Automata.Ui/Overlay/D2dNativeMethods.cs rename to src/Nexus.Ui/Overlay/D2dNativeMethods.cs index f33e9f7..f37496b 100644 --- a/src/Automata.Ui/Overlay/D2dNativeMethods.cs +++ b/src/Nexus.Ui/Overlay/D2dNativeMethods.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Automata.Ui.Overlay; +namespace Nexus.Ui.Overlay; /// Win32 P/Invoke for the D2D overlay window, DWM transparency, and frame timing. internal static partial class D2dNativeMethods diff --git a/src/Automata.Ui/Overlay/D2dOverlay.cs b/src/Nexus.Ui/Overlay/D2dOverlay.cs similarity index 98% rename from src/Automata.Ui/Overlay/D2dOverlay.cs rename to src/Nexus.Ui/Overlay/D2dOverlay.cs index 5cc155b..1bcd18e 100644 --- a/src/Automata.Ui/Overlay/D2dOverlay.cs +++ b/src/Nexus.Ui/Overlay/D2dOverlay.cs @@ -1,13 +1,13 @@ using System.Diagnostics; using System.Runtime; using System.Runtime.InteropServices; -using Automata.Bot; -using Automata.Ui.Overlay.Layers; -using Roboto.Memory; +using Nexus.Bot; +using Nexus.Ui.Overlay.Layers; +using Nexus.Memory; using Vortice.Mathematics; -using static Automata.Ui.Overlay.D2dNativeMethods; +using static Nexus.Ui.Overlay.D2dNativeMethods; -namespace Automata.Ui.Overlay; +namespace Nexus.Ui.Overlay; /// /// Fullscreen transparent overlay rendered with Direct2D on a dedicated thread. diff --git a/src/Automata.Ui/Overlay/D2dRenderContext.cs b/src/Nexus.Ui/Overlay/D2dRenderContext.cs similarity index 99% rename from src/Automata.Ui/Overlay/D2dRenderContext.cs rename to src/Nexus.Ui/Overlay/D2dRenderContext.cs index c81fa3c..d0cd9df 100644 --- a/src/Automata.Ui/Overlay/D2dRenderContext.cs +++ b/src/Nexus.Ui/Overlay/D2dRenderContext.cs @@ -6,7 +6,7 @@ using DWriteFactory = Vortice.DirectWrite.IDWriteFactory; using D2dFactoryType = Vortice.Direct2D1.FactoryType; using DwFactoryType = Vortice.DirectWrite.FactoryType; -namespace Automata.Ui.Overlay; +namespace Nexus.Ui.Overlay; public sealed class D2dRenderContext : IDisposable { diff --git a/src/Automata.Ui/Overlay/IOverlayLayer.cs b/src/Nexus.Ui/Overlay/IOverlayLayer.cs similarity index 91% rename from src/Automata.Ui/Overlay/IOverlayLayer.cs rename to src/Nexus.Ui/Overlay/IOverlayLayer.cs index b220771..bd210ac 100644 --- a/src/Automata.Ui/Overlay/IOverlayLayer.cs +++ b/src/Nexus.Ui/Overlay/IOverlayLayer.cs @@ -1,7 +1,7 @@ -using Automata.Navigation; -using Automata.Screen; +using Nexus.Navigation; +using Nexus.Screen; -namespace Automata.Ui.Overlay; +namespace Nexus.Ui.Overlay; public record OverlayState( IReadOnlyList Enemies, diff --git a/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs b/src/Nexus.Ui/Overlay/Layers/D2dDebugTextLayer.cs similarity index 99% rename from src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs rename to src/Nexus.Ui/Overlay/Layers/D2dDebugTextLayer.cs index f5d6c7f..e9f3008 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dDebugTextLayer.cs +++ b/src/Nexus.Ui/Overlay/Layers/D2dDebugTextLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Automata.Ui.Overlay.Layers; +namespace Nexus.Ui.Overlay.Layers; internal sealed class D2dDebugTextLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs b/src/Nexus.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs similarity index 99% rename from src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs rename to src/Nexus.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs index 13c0eb1..2a29ace 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs +++ b/src/Nexus.Ui/Overlay/Layers/D2dEnemyBoxLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Automata.Ui.Overlay.Layers; +namespace Nexus.Ui.Overlay.Layers; internal sealed class D2dEnemyBoxLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs b/src/Nexus.Ui/Overlay/Layers/D2dEntityLabelLayer.cs similarity index 94% rename from src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs rename to src/Nexus.Ui/Overlay/Layers/D2dEntityLabelLayer.cs index b5fcf77..19c40d4 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs +++ b/src/Nexus.Ui/Overlay/Layers/D2dEntityLabelLayer.cs @@ -1,10 +1,10 @@ using System.Drawing; using System.Numerics; -using Automata.Ui.ViewModels; -using Roboto.Data; +using Nexus.Ui.ViewModels; +using Nexus.Data; using Vortice.DirectWrite; -namespace Automata.Ui.Overlay.Layers; +namespace Nexus.Ui.Overlay.Layers; internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable { @@ -23,10 +23,10 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable public void Draw(D2dRenderContext ctx, OverlayState state) { - var data = RobotoViewModel.OverlayData; + var data = NexusViewModel.OverlayData; if (data is null || data.Entries.Length == 0) return; - var cache = RobotoViewModel.SharedCache; + var cache = NexusViewModel.SharedCache; if (cache is null) return; // Read camera and player position from centralized cache (updated at 60Hz) diff --git a/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs b/src/Nexus.Ui/Overlay/Layers/D2dHudInfoLayer.cs similarity index 99% rename from src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs rename to src/Nexus.Ui/Overlay/Layers/D2dHudInfoLayer.cs index 6381856..9dfa043 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dHudInfoLayer.cs +++ b/src/Nexus.Ui/Overlay/Layers/D2dHudInfoLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Automata.Ui.Overlay.Layers; +namespace Nexus.Ui.Overlay.Layers; internal sealed class D2dHudInfoLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs b/src/Nexus.Ui/Overlay/Layers/D2dLootLabelLayer.cs similarity index 98% rename from src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs rename to src/Nexus.Ui/Overlay/Layers/D2dLootLabelLayer.cs index 3ebfdcd..d716eb2 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dLootLabelLayer.cs +++ b/src/Nexus.Ui/Overlay/Layers/D2dLootLabelLayer.cs @@ -3,7 +3,7 @@ using Vortice.Direct2D1; using Vortice.DirectWrite; using Vortice.Mathematics; -namespace Automata.Ui.Overlay.Layers; +namespace Nexus.Ui.Overlay.Layers; internal sealed class D2dLootLabelLayer : ID2dOverlayLayer, IDisposable { diff --git a/src/Automata.Ui/Program.cs b/src/Nexus.Ui/Program.cs similarity index 94% rename from src/Automata.Ui/Program.cs rename to src/Nexus.Ui/Program.cs index 171915b..1afa025 100644 --- a/src/Automata.Ui/Program.cs +++ b/src/Nexus.Ui/Program.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; using Avalonia; -using Automata.Core; +using Nexus.Core; -namespace Automata.Ui; +namespace Nexus.Ui; class Program { diff --git a/src/Automata.Ui/ViewModels/AtlasViewModel.cs b/src/Nexus.Ui/ViewModels/AtlasViewModel.cs similarity index 98% rename from src/Automata.Ui/ViewModels/AtlasViewModel.cs rename to src/Nexus.Ui/ViewModels/AtlasViewModel.cs index f2acc8b..b257fc4 100644 --- a/src/Automata.Ui/ViewModels/AtlasViewModel.cs +++ b/src/Nexus.Ui/ViewModels/AtlasViewModel.cs @@ -3,11 +3,11 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Bot; -using Automata.Navigation; +using Nexus.Bot; +using Nexus.Navigation; using Serilog; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class AtlasViewModel : ObservableObject, IDisposable { diff --git a/src/Automata.Ui/ViewModels/CraftingViewModel.cs b/src/Nexus.Ui/ViewModels/CraftingViewModel.cs similarity index 99% rename from src/Automata.Ui/ViewModels/CraftingViewModel.cs rename to src/Nexus.Ui/ViewModels/CraftingViewModel.cs index e44b1d6..846b16a 100644 --- a/src/Automata.Ui/ViewModels/CraftingViewModel.cs +++ b/src/Nexus.Ui/ViewModels/CraftingViewModel.cs @@ -3,12 +3,12 @@ using System.IO; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Bot; -using Automata.Core; -using Automata.Items; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Items; using Serilog; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class CraftStepViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/DebugViewModel.cs b/src/Nexus.Ui/ViewModels/DebugViewModel.cs similarity index 99% rename from src/Automata.Ui/ViewModels/DebugViewModel.cs rename to src/Nexus.Ui/ViewModels/DebugViewModel.cs index 43dfce5..6760970 100644 --- a/src/Automata.Ui/ViewModels/DebugViewModel.cs +++ b/src/Nexus.Ui/ViewModels/DebugViewModel.cs @@ -1,12 +1,12 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Bot; -using Automata.Core; -using Automata.Game; -using Automata.Screen; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Game; +using Nexus.Screen; using Serilog; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class DebugViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs b/src/Nexus.Ui/ViewModels/MainWindowViewModel.cs similarity index 98% rename from src/Automata.Ui/ViewModels/MainWindowViewModel.cs rename to src/Nexus.Ui/ViewModels/MainWindowViewModel.cs index f75d7f5..017179f 100644 --- a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/Nexus.Ui/ViewModels/MainWindowViewModel.cs @@ -4,12 +4,12 @@ using System.Runtime.InteropServices; using Avalonia.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Bot; -using Automata.Core; -using Automata.Navigation; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Navigation; using Serilog; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public class LogEntry { @@ -183,7 +183,7 @@ public partial class MainWindowViewModel : ObservableObject public AtlasViewModel? AtlasVm { get; set; } public CraftingViewModel? CraftingVm { get; set; } public MemoryViewModel? MemoryVm { get; set; } - public RobotoViewModel? RobotoVm { get; set; } + public NexusViewModel? NexusVm { get; set; } public ObjectBrowserViewModel? BrowserVm { get; set; } partial void OnBotModeChanged(BotMode value) diff --git a/src/Automata.Ui/ViewModels/MappingViewModel.cs b/src/Nexus.Ui/ViewModels/MappingViewModel.cs similarity index 97% rename from src/Automata.Ui/ViewModels/MappingViewModel.cs rename to src/Nexus.Ui/ViewModels/MappingViewModel.cs index 0514963..5ab9f3d 100644 --- a/src/Automata.Ui/ViewModels/MappingViewModel.cs +++ b/src/Nexus.Ui/ViewModels/MappingViewModel.cs @@ -2,11 +2,11 @@ using System.Collections.ObjectModel; using Timer = System.Timers.Timer; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; -using Automata.Bot; -using Automata.Core; -using Automata.Screen; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Screen; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class MappingViewModel : ObservableObject, IDisposable { diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Nexus.Ui/ViewModels/MemoryViewModel.cs similarity index 99% rename from src/Automata.Ui/ViewModels/MemoryViewModel.cs rename to src/Nexus.Ui/ViewModels/MemoryViewModel.cs index 86093a3..d16b4b1 100644 --- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs +++ b/src/Nexus.Ui/ViewModels/MemoryViewModel.cs @@ -6,10 +6,10 @@ using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Roboto.Memory; -using Roboto.Memory.Objects; +using Nexus.Memory; +using Nexus.Memory.Objects; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class MemoryNodeViewModel : ObservableObject { @@ -646,7 +646,7 @@ public partial class MemoryViewModel : ObservableObject { var withPos = snap.Entities.Count(e => e.HasPosition); var withComps = snap.Entities.Count(e => e.Components is not null); - var monsters = snap.Entities.Count(e => e.Type == Roboto.Memory.EntityType.Monster); + var monsters = snap.Entities.Count(e => e.Type == Nexus.Memory.EntityType.Monster); var knownComps = _reader?.Registry["components"].Count ?? 0; _entitySummary!.Set($"{snap.Entities.Count} total, {withComps} with comps, {knownComps} known, {monsters} monsters"); @@ -723,7 +723,7 @@ public partial class MemoryViewModel : ObservableObject /// from live memory and recurses. For collapsed nodes with children, ensures /// a placeholder exists so the expand arrow is visible. /// - private static void SyncUiNodeLazy(MemoryNodeViewModel vm, Roboto.Memory.Objects.UIElements uiElements) + private static void SyncUiNodeLazy(MemoryNodeViewModel vm, Nexus.Memory.Objects.UIElements uiElements) { var uiEl = vm.UiElement; if (uiEl is null || uiEl.Address == 0) return; diff --git a/src/Automata.Ui/ViewModels/ModPoolViewModel.cs b/src/Nexus.Ui/ViewModels/ModPoolViewModel.cs similarity index 93% rename from src/Automata.Ui/ViewModels/ModPoolViewModel.cs rename to src/Nexus.Ui/ViewModels/ModPoolViewModel.cs index 9233ef1..e815105 100644 --- a/src/Automata.Ui/ViewModels/ModPoolViewModel.cs +++ b/src/Nexus.Ui/ViewModels/ModPoolViewModel.cs @@ -1,7 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Automata.Core; +using Nexus.Core; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class AvailableModViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/RobotoViewModel.cs b/src/Nexus.Ui/ViewModels/NexusViewModel.cs similarity index 98% rename from src/Automata.Ui/ViewModels/RobotoViewModel.cs rename to src/Nexus.Ui/ViewModels/NexusViewModel.cs index 6bd17a1..711db01 100644 --- a/src/Automata.Ui/ViewModels/RobotoViewModel.cs +++ b/src/Nexus.Ui/ViewModels/NexusViewModel.cs @@ -3,17 +3,17 @@ using System.Numerics; using System.Runtime.InteropServices; using Avalonia; using Avalonia.Media.Imaging; -using Roboto.Memory; +using Nexus.Memory; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.GameLog; -using Roboto.Core; -using Roboto.Data; -using Roboto.Engine; -using Roboto.Input; -using Roboto.Navigation; +using Nexus.GameLog; +using Nexus.Core; +using Nexus.Data; +using Nexus.Engine; +using Nexus.Input; +using Nexus.Pathfinding; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; /// /// Thread-safe snapshot read by the overlay layer each frame. @@ -79,7 +79,7 @@ public partial class EntityListItem : ObservableObject } } -public partial class RobotoViewModel : ObservableObject, IDisposable +public partial class NexusViewModel : ObservableObject, IDisposable { [LibraryImport("user32.dll")] private static partial short GetAsyncKeyState(int vKey); @@ -170,7 +170,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable /// public static volatile GameDataCache? SharedCache; - public RobotoViewModel(IClientLogWatcher logWatcher) + public NexusViewModel(IClientLogWatcher logWatcher) { var config = new BotConfig(); var reader = new GameMemoryReader(); diff --git a/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs b/src/Nexus.Ui/ViewModels/ObjectBrowserViewModel.cs similarity index 99% rename from src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs rename to src/Nexus.Ui/ViewModels/ObjectBrowserViewModel.cs index 778ebc2..5ccce1f 100644 --- a/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs +++ b/src/Nexus.Ui/ViewModels/ObjectBrowserViewModel.cs @@ -2,9 +2,9 @@ using System.Collections.ObjectModel; using System.Text; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Roboto.Memory; +using Nexus.Memory; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class FieldRowViewModel : ObservableObject { @@ -409,7 +409,7 @@ public partial class ObjectBrowserViewModel : ObservableObject /// /// Builds a map of offset → label name for the given object type, based on known GameOffsets. /// - private static Dictionary BuildOffsetLabels(GameOffsets o, string objectType) + private static Dictionary BuildOffsetLabels(Nexus.Memory.GameOffsets o, string objectType) { // Normalize: strip namespace prefixes (e.g. "GameStates@InGameState" → "InGameState") var type = objectType; diff --git a/src/Automata.Ui/ViewModels/ParsedModViewModel.cs b/src/Nexus.Ui/ViewModels/ParsedModViewModel.cs similarity index 97% rename from src/Automata.Ui/ViewModels/ParsedModViewModel.cs rename to src/Nexus.Ui/ViewModels/ParsedModViewModel.cs index 18ae28f..9458f0c 100644 --- a/src/Automata.Ui/ViewModels/ParsedModViewModel.cs +++ b/src/Nexus.Ui/ViewModels/ParsedModViewModel.cs @@ -1,7 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Sidekick.Data.Items; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class ParsedModViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/SettingsViewModel.cs b/src/Nexus.Ui/ViewModels/SettingsViewModel.cs similarity index 99% rename from src/Automata.Ui/ViewModels/SettingsViewModel.cs rename to src/Nexus.Ui/ViewModels/SettingsViewModel.cs index e7d15f5..8080cf6 100644 --- a/src/Automata.Ui/ViewModels/SettingsViewModel.cs +++ b/src/Nexus.Ui/ViewModels/SettingsViewModel.cs @@ -4,12 +4,12 @@ using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Bot; -using Automata.Core; -using Automata.Inventory; +using Nexus.Bot; +using Nexus.Core; +using Nexus.Inventory; using Serilog; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class SettingsViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/SkillProfileViewModel.cs b/src/Nexus.Ui/ViewModels/SkillProfileViewModel.cs similarity index 98% rename from src/Automata.Ui/ViewModels/SkillProfileViewModel.cs rename to src/Nexus.Ui/ViewModels/SkillProfileViewModel.cs index 6f685f6..5303369 100644 --- a/src/Automata.Ui/ViewModels/SkillProfileViewModel.cs +++ b/src/Nexus.Ui/ViewModels/SkillProfileViewModel.cs @@ -1,9 +1,9 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Roboto.Core; +using Nexus.Core; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class SkillProfileViewModel : ObservableObject { diff --git a/src/Automata.Ui/ViewModels/StashTabViewModel.cs b/src/Nexus.Ui/ViewModels/StashTabViewModel.cs similarity index 95% rename from src/Automata.Ui/ViewModels/StashTabViewModel.cs rename to src/Nexus.Ui/ViewModels/StashTabViewModel.cs index 0685056..c9b7d1d 100644 --- a/src/Automata.Ui/ViewModels/StashTabViewModel.cs +++ b/src/Nexus.Ui/ViewModels/StashTabViewModel.cs @@ -1,8 +1,8 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; -using Automata.Core; +using Nexus.Core; -namespace Automata.Ui.ViewModels; +namespace Nexus.Ui.ViewModels; public partial class StashTabViewModel : ObservableObject { diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Nexus.Ui/Views/MainWindow.axaml similarity index 99% rename from src/Automata.Ui/Views/MainWindow.axaml rename to src/Nexus.Ui/Views/MainWindow.axaml index b47aa9e..e9a5163 100644 --- a/src/Automata.Ui/Views/MainWindow.axaml +++ b/src/Nexus.Ui/Views/MainWindow.axaml @@ -1,9 +1,9 @@ @@ -845,9 +845,9 @@ - - - + + + - - + + - + true/pm diff --git a/src/Roboto.Core/Roboto.Core.csproj b/src/Roboto.Core/Roboto.Core.csproj deleted file mode 100644 index b104a1a..0000000 --- a/src/Roboto.Core/Roboto.Core.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - net8.0-windows10.0.19041.0 - enable - enable - - From 05bbcb244f008d3f8fac9a592aa5351d69c1e1d7 Mon Sep 17 00:00:00 2001 From: Boki Date: Sat, 7 Mar 2026 09:53:57 -0500 Subject: [PATCH 2/5] simulation done --- docs/architecture-overview.md | 188 ++++++ docs/core.md | 158 +++++ docs/data-and-memory.md | 229 +++++++ docs/engine-and-systems.md | 197 ++++++ docs/infrastructure.md | 207 +++++++ docs/input.md | 67 ++ docs/pathfinding.md | 153 +++++ docs/simulator.md | 180 ++++++ docs/test.html | 95 +++ imgui.ini | 2 +- src/Nexus.Core/ActionExecutor.cs | 52 ++ src/Nexus.Core/ActionQueue.cs | 23 +- src/Nexus.Core/BotConfig.cs | 4 +- src/Nexus.Core/ISystem.cs | 2 +- src/Nexus.Core/Logging.cs | 2 +- src/Nexus.Core/MovementBlender.cs | 231 +++++++ src/Nexus.Core/MovementKeyTracker.cs | 122 ++++ src/Nexus.Core/ScanCodes.cs | 56 ++ src/Nexus.Core/TerrainQuery.cs | 50 +- src/Nexus.Core/WalkabilitySnapshot.cs | 19 +- src/Nexus.Data/GameStateEnricher.cs | 90 ++- .../InterceptionInputController.cs | 35 +- src/Nexus.Input/ScanCodes.cs | 96 ++- src/Nexus.Input/SendInputController.cs | 37 +- src/Nexus.Pathfinding/NavigationController.cs | 210 +++++-- src/Nexus.Pathfinding/PathFinder.cs | 36 +- .../Bridge/SimInputController.cs | 134 +++- src/Nexus.Simulator/Bridge/SimPoller.cs | 3 +- src/Nexus.Simulator/Bridge/SimStateBuilder.cs | 2 + src/Nexus.Simulator/Config/SimConfig.cs | 42 +- src/Nexus.Simulator/Program.cs | 139 ++--- src/Nexus.Simulator/Rendering/DebugPanel.cs | 11 +- .../Rendering/EffectRenderer.cs | 33 +- .../Rendering/EntityRenderer.cs | 51 +- .../Rendering/InputOverlayRenderer.cs | 155 +++++ src/Nexus.Simulator/Rendering/PathRenderer.cs | 17 +- src/Nexus.Simulator/Rendering/SimRenderer.cs | 81 ++- .../Rendering/TerrainRenderer.cs | 84 ++- .../Rendering/VeldridImGuiRenderer.cs | 138 ++--- .../Rendering/ViewTransform.cs | 62 ++ src/Nexus.Simulator/World/SimEnemy.cs | 29 +- src/Nexus.Simulator/World/SimPlayer.cs | 61 +- src/Nexus.Simulator/World/SimProjectile.cs | 5 +- src/Nexus.Simulator/World/SimWorld.cs | 422 +++++++++---- src/Nexus.Simulator/World/TerrainGenerator.cs | 286 +++++---- src/Nexus.Systems/AreaProgressionSystem.cs | 582 ++++++++++++++++++ src/Nexus.Systems/BotTick.cs | 46 ++ src/Nexus.Systems/CombatSystem.cs | 45 +- src/Nexus.Systems/LootSystem.cs | 2 +- src/Nexus.Systems/MovementSystem.cs | 71 ++- src/Nexus.Systems/NavigationSystem.cs | 4 +- src/Nexus.Systems/Nexus.Systems.csproj | 2 + src/Nexus.Systems/ResourceSystem.cs | 2 +- src/Nexus.Systems/SystemFactory.cs | 35 ++ src/Nexus.Systems/ThreatSystem.cs | 38 +- 55 files changed, 4367 insertions(+), 756 deletions(-) create mode 100644 docs/architecture-overview.md create mode 100644 docs/core.md create mode 100644 docs/data-and-memory.md create mode 100644 docs/engine-and-systems.md create mode 100644 docs/infrastructure.md create mode 100644 docs/input.md create mode 100644 docs/pathfinding.md create mode 100644 docs/simulator.md create mode 100644 docs/test.html create mode 100644 src/Nexus.Core/ActionExecutor.cs create mode 100644 src/Nexus.Core/MovementBlender.cs create mode 100644 src/Nexus.Core/MovementKeyTracker.cs create mode 100644 src/Nexus.Core/ScanCodes.cs create mode 100644 src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs create mode 100644 src/Nexus.Simulator/Rendering/ViewTransform.cs create mode 100644 src/Nexus.Systems/AreaProgressionSystem.cs create mode 100644 src/Nexus.Systems/BotTick.cs create mode 100644 src/Nexus.Systems/SystemFactory.cs diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md new file mode 100644 index 0000000..04e79de --- /dev/null +++ b/docs/architecture-overview.md @@ -0,0 +1,188 @@ +# Nexus — Architecture Overview + +## What Is This + +A modular C# automation framework for POE2. The system reads game memory, builds a structured game state, runs AI systems (combat, navigation, threat assessment), and emits input commands. A separate trade pipeline monitors the trade site via a Node.js daemon and executes buy flows. + +When the real game isn't available, a standalone **Simulator** replaces the memory layer with a procedural game world — same bot systems, same interfaces, zero game dependency. + +## Solution Structure + +``` +Nexus.sln (net8.0-windows10.0.19041.0) +│ +├── Core Layer (shared types, no dependencies) +│ └── Nexus.Core +│ +├── Infrastructure Layer (reads from external sources) +│ ├── Nexus.GameOffsets Pure offset structs for memory reading +│ ├── Nexus.Memory Process memory reading (RPM, pattern scan) +│ ├── Nexus.Screen Screen capture, OCR, grid detection +│ ├── Nexus.Log Client.txt game log watcher +│ └── Nexus.Input Win32 SendInput / Interception driver +│ +├── Data Layer (transforms raw data into typed game state) +│ └── Nexus.Data EntityMapper, EntityClassifier, GameDataCache, MemoryPoller, GameStateEnricher +│ +├── Logic Layer (AI systems that decide what to do) +│ ├── Nexus.Systems ThreatSystem, MovementSystem, CombatSystem, ResourceSystem, LootSystem +│ ├── Nexus.Engine BotEngine (orchestrator), AreaProgressionSystem, MovementKeyTracker +│ └── Nexus.Pathfinding NavigationController, A* PathFinder +│ +├── Game Interaction Layer (acts on the game) +│ ├── Nexus.Game Window focus, input sending, clipboard +│ ├── Nexus.Items Item parsing via Sidekick +│ ├── Nexus.Inventory Stash/inventory grid management +│ ├── Nexus.Navigation Minimap-based real-time navigation +│ └── Nexus.Trade Trade daemon IPC (Node.js Playwright) +│ +├── Orchestration Layer (top-level coordination) +│ ├── Nexus.Bot BotOrchestrator, Trade/Mapping/Crafting executors +│ └── Nexus.Ui Avalonia 11.2 desktop GUI (entry point) +│ +└── Testing Layer + └── Nexus.Simulator Standalone game world for bot testing +``` + +## Dependency Flow + +``` +Nexus.Core + │ + ├── Nexus.GameOffsets ──→ Nexus.Memory ──→ Nexus.Data ──→ Nexus.Engine + │ │ │ + ├── Nexus.Input │ Nexus.Systems + │ │ │ + ├── Nexus.Screen ◄───────────────────────────────┘ │ + ├── Nexus.Game │ + ├── Nexus.Log │ + │ │ + ├── Nexus.Pathfinding ◄─────────────────────────────────────────┘ + │ + ├── Nexus.Items, Nexus.Inventory, Nexus.Navigation, Nexus.Trade + │ + ├── Nexus.Bot (consumes all above) + │ + ├── Nexus.Ui (DI hub, entry point, consumes all) + │ + └── Nexus.Simulator (Core, Data, Systems, Pathfinding — NOT Memory/Input/Screen) +``` + +## Data Flow — Per Tick + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ MemoryPoller Thread (60Hz hot / 10Hz cold) │ +│ │ +│ Hot tick (4 RPM calls): │ +│ Camera matrix, Player position, Player vitals, Loading state │ +│ │ +│ Cold tick (full hierarchical read): │ +│ Entity tree traversal → classification → EntitySnapshot[] │ +│ Terrain grid, Skills, Quests, UI elements │ +│ │ +│ Writes to GameDataCache (volatile references, lock-free) │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ BotEngine Logic Thread (25Hz default) │ +│ │ +│ 1. Read latest GameState from cache │ +│ 2. GameStateEnricher → NearestEnemies, ThreatMap, DangerLevel │ +│ 3. Clear ActionQueue │ +│ 4. NavigationController.Update() → path, DesiredDirection │ +│ 5. Run systems in priority order: │ +│ ThreatSystem (50) → MovementSystem (100) → │ +│ AreaProgressionSystem (199) → NavigationSystem (200) → │ +│ CombatSystem (300) → ResourceSystem (400) → LootSystem (500) │ +│ 6. ActionQueue.Resolve() → conflict resolution │ +│ 7. ExecuteActions() → IInputController │ +└────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ IInputController (Win32 SendInput or Interception driver) │ +│ │ +│ WASD keys (scan codes), mouse movement (Bézier curves), │ +│ skill casts, flask presses, clicks │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +## Action Resolution + +Systems submit actions to a shared `ActionQueue`. The queue resolves conflicts: + +| Action Type | Behavior | +|-------------|----------| +| FlaskAction | Always passes through | +| MoveAction (priority ≤ 10) | Urgent flee — blocks CastAction | +| MoveAction (priority > 10) | Normal move — allows CastAction alongside | +| CastAction | Passes unless blocked by urgent flee | +| Other (Key, Click, Chat) | Always passes through | + +Priority values: lower number = higher priority. ThreatSystem uses priority 5 for emergency flee (blocks all casting). + +## System Priority Table + +| Priority | System | Purpose | +|----------|--------|---------| +| 50 | ThreatSystem | Emergency flee on High/Critical danger | +| 100 | MovementSystem | Soft avoidance via inverse-square repulsion | +| 199 | AreaProgressionSystem | Quest-driven area traversal and transitions | +| 200 | NavigationSystem | Submits NavigationController's direction | +| 300 | CombatSystem | Skill rotation, target selection, kiting | +| 400 | ResourceSystem | Flask usage on life/mana thresholds | +| 500 | LootSystem | Item pickup (stub) | + +## Thread Model + +| Thread | Rate | Responsibility | +|--------|------|----------------| +| MemoryPoller | 60Hz hot, 10Hz cold | Read game memory → GameDataCache | +| BotEngine Logic | 25Hz | Run AI systems → emit actions | +| Render (Simulator only) | vsync | ImGui + Veldrid drawing | +| Trade Daemon | event-driven | Node.js Playwright → stdin/stdout JSON IPC | +| Log Watcher | 200ms poll | Client.txt → area/whisper/trade events | + +Cross-thread safety: GameDataCache uses `volatile` references. No locks — writer (MemoryPoller) atomically swaps reference types, readers (BotEngine) get consistent snapshots. + +## Simulator Architecture + +When the real game isn't available, `Nexus.Simulator` replaces the memory pipeline: + +``` +┌─────────────────────────────────────────────────────┐ +│ Nexus.Simulator.exe │ +│ │ +│ ┌──────────┐ GameState ┌──────────────────┐ │ +│ │ SimWorld │───────────────►│ GameDataCache │ │ +│ │ (terrain, │ SimPoller + │ (same as prod) │ │ +│ │ enemies, │ StateBuilder └────────┬─────────┘ │ +│ │ player) │ │ │ +│ └─────┬────┘ ┌────────▼─────────┐ │ +│ │ │ Bot Systems │ │ +│ │◄─────────────────────│ (unchanged) │ │ +│ │ SimInputController └──────────────────┘ │ +│ │ │ +│ ┌─────▼───────────────────────────────────────┐ │ +│ │ ImGui + Veldrid Renderer (isometric 2D) │ │ +│ └─────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────┘ +``` + +Bot systems don't know they're in a simulation — they see identical `GameState` objects and emit actions to `IInputController`. + +## Key Design Decisions + +| Decision | Rationale | +|----------|-----------| +| Immutable records (GameState, EntitySnapshot) | Thread-safe sharing without locks | +| Priority-based ActionQueue | Natural conflict resolution without hardcoded if-else | +| Two-tier memory polling (hot/cold) | Balance responsiveness (position at 60Hz) with CPU cost (entities at 10Hz) | +| Scan codes over virtual keys | Games read hardware scan codes, not VK codes | +| Separate Memory → Data layers | Memory reads raw bytes; Data interprets and classifies. Clean testability. | +| IInputController interface | Swap real Win32 input for simulated input without changing bot logic | +| BFS exploration + A* pathfinding | BFS finds what to explore; A* finds how to get there | +| CharacterProfile auto-detection | Automatically applies combat/flask settings per character name | +| External daemons (Trade, OCR) | Isolate browser/OCR concerns from main process | diff --git a/docs/core.md b/docs/core.md new file mode 100644 index 0000000..e3dfa68 --- /dev/null +++ b/docs/core.md @@ -0,0 +1,158 @@ +# Nexus.Core — Shared Types & Abstractions + +The foundation layer. Every other project depends on Core. Contains no logic — only types, interfaces, configuration, and utilities. + +## GameState + +Central immutable snapshot updated once per tick. All systems read from this; none mutate it. + +``` +GameState +├── Timing: TickNumber, DeltaTime, TimestampMs +├── Player: PlayerState +│ ├── CharacterName, Position, Z, HasPosition +│ ├── Life/Mana/ES: Current, Total, Percent (derived) +│ ├── ActionId (actor animation state) +│ ├── Skills: SkillState[] (cooldowns, charges, cast state) +│ ├── Flasks: FlaskState[] (charges, active, cooldown) +│ └── Buffs: Buff[] (name, duration, charges, isDebuff) +├── Entities: EntitySnapshot[] (all game entities) +├── HostileMonsters: EntitySnapshot[] (alive monsters, filtered) +├── NearbyLoot: EntitySnapshot[] (world items, filtered) +├── NearestEnemies: EntitySnapshot[] (sorted by distance — enriched) +├── Terrain: WalkabilitySnapshot (grid, offsets) +├── Area: AreaHash, AreaLevel, CurrentAreaName +├── UI: IsLoading, IsEscapeOpen, CameraMatrix +├── Quests: ActiveQuests[], UiQuests[], Quests[] (enriched with paths) +├── Threats: ThreatMap (zone buckets, centroid, rarity flags) +├── Danger: DangerLevel (Safe/Low/Medium/High/Critical — enriched) +└── GroundEffects: GroundEffect[] (hazard positions) +``` + +## EntitySnapshot + +Immutable record for any game entity: + +- **Identity**: Id (uint), Path, Metadata, Category (EntityCategory enum) +- **Spatial**: Position (Vector2), Z, DistanceToPlayer +- **Combat**: IsAlive, LifeCurrent/Total, IsTargetable, ThreatLevel, Rarity, ModNames +- **State**: ActionId, IsAttacking, IsMoving, Components (HashSet) +- **Special**: TransitionName/State, ItemBaseName, IsQuestItem, LabelOffset + +**EntityCategory** (17 types): Unknown, Player, Monster, Npc, WorldItem, Chest, Shrine, Portal, AreaTransition, Effect, Terrain, MiscObject, Waypoint, Door, Doodad, TownPortal, Critter + +**MonsterRarity**: White, Magic, Rare, Unique + +**MonsterThreatLevel**: None, Normal, Magic, Rare, Unique + +## Actions + +Polymorphic action system. All inherit from `BotAction` with a `Priority` field. + +| Action | Fields | Purpose | +|--------|--------|---------| +| MoveAction | Direction (Vector2) | WASD movement | +| CastAction | SkillScanCode, TargetScreenPos or TargetEntityId | Skill usage | +| FlaskAction | FlaskScanCode | Flask consumption | +| KeyAction | ScanCode, Type (Press/Down/Up) | Raw keyboard | +| ClickAction | ScreenPosition, Type (Left/Right/Middle) | Mouse click | +| ChatAction | Message | Send chat message | +| WaitAction | DurationMs | Delay | + +## ActionQueue + +Manages conflict resolution between competing system outputs. + +**Resolve() rules:** +1. FlaskActions always pass through +2. Get highest-priority MoveAction and CastAction +3. If MoveAction priority ≤ 10 (urgent flee): include move, **block** cast +4. Else: include both move and cast +5. All other action types pass through + +## ISystem Interface + +```csharp +public interface ISystem +{ + int Priority { get; } // Execution order (lower = first) + string Name { get; } + bool IsEnabled { get; set; } + void Update(GameState state, ActionQueue actions); +} +``` + +**SystemPriority constants**: Threat=50, Movement=100, Navigation=200, Combat=300, Resource=400, Loot=500 + +## IInputController Interface + +Abstraction over Win32 input. Two implementations: SendInputController (vanilla), InterceptionInputController (driver). + +```csharp +public interface IInputController +{ + bool IsInitialized { get; } + void KeyDown(ushort scanCode); + void KeyUp(ushort scanCode); + void KeyPress(ushort scanCode, int holdMs = 50); + void MouseMoveTo(int x, int y); + void SmoothMoveTo(int x, int y); // Bézier curve interpolation + void LeftClick(int x, int y); + void RightClick(int x, int y); + void MiddleClick(int x, int y); + void LeftDown(); void LeftUp(); + void RightDown(); void RightUp(); +} +``` + +## WalkabilitySnapshot + +Grid-based terrain with offset support for infinite expansion: + +```csharp +public record WalkabilitySnapshot +{ + public int Width, Height; + public byte[] Data; // Row-major; 0=wall, nonzero=walkable + public int OffsetX, OffsetY; // Absolute grid coords of top-left corner + + public bool IsWalkable(int gx, int gy) + { + var lx = gx - OffsetX; // Absolute → local + var ly = gy - OffsetY; + if (out of bounds) return false; + return Data[ly * Width + lx] != 0; + } +} +``` + +Coordinate conversion: `WorldToGrid = 23f / 250f ≈ 0.092` +- World → Grid: `gx = (int)(worldX * WorldToGrid)` +- Grid → World: `worldX = gx / WorldToGrid` + +## Configuration + +**BotConfig** — Static bot parameters: +- Tick rates: LogicTickRateHz=60, MemoryPollRateHz=30 +- Movement: SafeDistance=400, RepulsionWeight=1.5, WaypointReachedDistance=80 +- Humanization: MinReactionDelayMs=50, MaxReactionDelayMs=150, ClickJitterRadius=3, MaxApm=250 + +**CharacterProfile** — Per-character settings: +- Skills (8 slots: LMB, RMB, MMB, Q, E, R, T, F) with priority, cooldown, range, target selection +- Flasks (thresholds, scan codes, cooldown) +- Combat (global cooldown, attack/safe/kite ranges) + +**SkillProfile** — Per-skill configuration: +- InputType (KeyPress/LeftClick/RightClick/MiddleClick) +- TargetSelection (Nearest/All/Rarest/MagicPlus/RarePlus/UniqueOnly) +- RequiresTarget, IsAura, IsMovementSkill, MaintainPressed +- MinMonstersInRange (AOE threshold) + +## Utilities + +- **WorldToScreen.Project()** — Matrix projection: world coords → screen coords via camera matrix +- **TerrainQuery.HasLineOfSight()** — Bresenham line walk on walkability grid +- **TerrainQuery.FindWalkableDirection()** — Rotates direction ±45°/90°/135°/180° to find clear path +- **Helpers.Sleep()** — Task delay with ±10% variance +- **DangerLevel**: Safe, Low, Medium, High, Critical +- **ThreatMap**: TotalHostiles, CloseRange(<300), MidRange(300-600), FarRange(600-1200), ClosestDistance, ThreatCentroid, HasRareOrUnique diff --git a/docs/data-and-memory.md b/docs/data-and-memory.md new file mode 100644 index 0000000..82f9a2b --- /dev/null +++ b/docs/data-and-memory.md @@ -0,0 +1,229 @@ +# Nexus.Data & Nexus.Memory — The Data Pipeline + +## Overview + +Two-layer architecture: **Memory** reads raw bytes from the game process; **Data** interprets, classifies, and caches them. This separation keeps memory reading logic free of business rules and makes the data layer testable independently. + +``` +Game Process (RPM) + │ + ▼ +Nexus.Memory (raw reads, no business logic) + │ GameMemoryReader → hierarchical state tree + │ EntityList → red-black tree traversal + │ ComponentReader → ECS component extraction + │ + ▼ +Nexus.Data (interpretation, classification, caching) + │ MemoryPoller → two-tier event loop + │ EntityMapper → Memory.Entity → Core.EntitySnapshot + │ EntityClassifier → path + components → EntityCategory + │ GameStateEnricher → derived threat/danger metrics + │ + ▼ +GameDataCache (volatile references, lock-free) + │ + ▼ +Bot Systems (read-only consumers) +``` + +--- + +## GameDataCache — Single Source of Truth + +Thread-safe, volatile reference holder. Writer: MemoryPoller thread. Readers: bot systems. + +**Hot fields** (updated at 60Hz — 4 lightweight RPM calls): +- CameraMatrix (64 bytes) +- PlayerPosition (X, Y, Z) +- PlayerVitals (HP, mana, ES current/max) +- IsLoading, IsEscapeOpen + +**Cold fields** (updated at 10Hz — full hierarchical read): +- Entities, HostileMonsters, NearbyLoot +- Terrain (WalkabilitySnapshot) +- AreaHash, AreaLevel, CurrentAreaName +- Quest data (linked lists, UI groups, state entries) +- LatestState (complete GameState) + +**Slow fields** (updated at 1Hz): +- Character name +- Quest linked lists, quest state entries + +No locks — relies on volatile reference semantics for atomic swaps. + +--- + +## MemoryPoller — Two-Tier Event Loop + +Owns the memory-reading background thread. + +### Hot Tick (60Hz) + +4 pre-resolved RPM calls using cached addresses: +1. Camera matrix (64 bytes from cached address) +2. Player position (12 bytes: X, Y, Z) +3. Player vitals (24 bytes: HP, mana, ES) +4. Loading/escape state (pointer dereference + int) + +No allocations, no GC. Targeting ~3-5ms per tick. + +### Cold Tick (10Hz, every 6th hot tick) + +Full hierarchical read: +1. `GameMemoryReader.ReadSnapshot()` — cascades through state tree +2. Re-resolve hot addresses via `ResolveHotAddresses()` +3. `BuildGameState()` — map entities, filter lists, process quests +4. `GameStateEnricher.Enrich()` — compute derived fields +5. Update all cache fields + +### BuildGameState() Flow + +``` +ReadSnapshot() → GameStateSnapshot (raw) + │ + ├── Entity mapping: + │ for each entity in snapshot: + │ EntityMapper.MapEntity(entity, playerPos) → EntitySnapshot + │ + ├── Filter into: + │ - HostileMonsters (Category==Monster && IsAlive) + │ - NearbyLoot (Category==WorldItem) + │ - All entities + │ + ├── Quest processing: + │ - Filter active (StateId > 0) + │ - Resolve state text via QuestStateLookup + │ - Convert to QuestProgress, QuestInfo + │ + └── Returns GameState +``` + +--- + +## GameMemoryReader — Hierarchical State Tree + +Top-level orchestrator. Creates sub-readers on `Attach()`: + +``` +GameStates (top) +└── InGameState + ├── AreaInstance + │ ├── EntityList (MSVC std::map red-black tree) + │ ├── PlayerSkills (Actor component) + │ ├── QuestStates (dat file entries) + │ └── Terrain (walkability grid) + ├── UIElements (quest linked lists, UI tree) + └── WorldData (camera matrix) +``` + +Each `RemoteObject` caches its data and depends on parent for context. Single `Update()` call cascades through tree. + +### Infrastructure + +- **ProcessMemory**: P/Invoke wrapper for ReadProcessMemory. Tracks reads/sec and KB/sec. +- **MemoryContext**: Shared state — process handle, offsets, module base, pattern scanner. +- **ComponentReader**: Reads ECS components (Life, Render, Mods, etc.) from entities. +- **MsvcStringReader**: Reads MSVC std::wstring (SSO-aware: inline if capacity ≤ 8, heap pointer otherwise). +- **PatternScanner**: AOB scan for resolving base addresses. + +--- + +## EntityList — Tree Traversal + +Reads entities from AreaInstance's MSVC std::map (red-black tree, in-order traversal). + +**Tree node layout:** +``` ++0x00: left child ptr ++0x08: parent ptr ++0x10: right child ptr ++0x28: entity pointer +``` + +**Optimization**: Tree order is cached — re-walked only when entity count changes. + +**Per-entity reads:** +1. Path (EntityDetails → std::wstring) +2. Skip low-priority types (effects, terrain, critters — no components read) +3. Position (Render component: X, Y, Z) +4. Component lookup (STL hash map: name → index) +5. Component data: + - Targetable (bool flag) + - Mods/ObjectMagicProperties (rarity) + - Life (HP, dynamic — re-read every frame for monsters) + - Actor (action ID) + - WorldItem (inner entity for ground loot) + - AreaTransition (destination area) + +**Caching strategy:** +- Stable per entity: path, component list, targetable, rarity, transition name +- Dynamic (re-read every frame): monster HP, action ID + +--- + +## EntityClassifier — Path + Components → Category + +Single source of truth for entity classification. + +1. **Path-based** (primary): Parses `Metadata/[Category]/...` path segments +2. **Component override**: Monster, Chest, Shrine, Waypoint, AreaTransition, Portal, TownPortal, NPC, Player + +Output: `EntityCategory` (Core enum, 17 types) + +--- + +## EntityMapper — Memory.Entity → Core.EntitySnapshot + +Transforms raw memory data to enriched snapshots: + +``` +Memory.Entity (raw) + │ + ├── Copy: ID, path, metadata, position, Z, vitals, components, mods + ├── Classify: EntityClassifier.Classify(path, components) → EntityCategory + ├── Threat level: Rarity → MonsterThreatLevel + ├── Area name: AreaNameLookup.Resolve() for transitions + ├── Distance: Vector2.Distance(position, playerPos) + └── Alive state: HasVitals ? LifeCurrent > 0 : true + │ + ▼ +Core.EntitySnapshot (public, classified, enriched) +``` + +--- + +## GameStateEnricher — Derived Metrics + +Computed once per cold tick, before systems run. + +**NearestEnemies**: HostileMonsters sorted by distance to player. + +**ThreatMap**: +- TotalHostiles, CloseRange (<300u), MidRange (300-600u), FarRange (600-1200u) +- ClosestDistance, ThreatCentroid (position average), HasRareOrUnique + +**DangerLevel** — Weighted threat score: +``` +score = Σ (distance_weight × rarity_multiplier) + +Distance weights: <200u = 3×, <400u = 2×, else = 1× +Rarity multipliers: Unique=5×, Rare=3×, Magic=1.5×, White=1× + +Life override: HP < 30% → Critical, HP < 50% → High +Score thresholds: ≥15 = Critical, ≥8 = High, ≥4 = Medium, >0 = Low, 0 = Safe +``` + +--- + +## Key Architectural Patterns + +| Pattern | Implementation | +|---------|---------------| +| Lock-free cross-thread | Volatile references in GameDataCache; no locks needed | +| Two-tier polling | Hot (4 RPM, 60Hz) + Cold (full read, 10Hz) | +| Hierarchical caching | Each RemoteObject caches data, re-reads only on change | +| Entity caching | Stable data cached per entity/zone; dynamic data (HP) re-read per frame | +| Separation of concerns | Memory: raw bytes. Data: interpretation + classification | +| Area name resolution | AreaNameLookup loads areas.json, caches ID → display name | +| Area graph | BFS pathfinding for quest progression ordering | diff --git a/docs/engine-and-systems.md b/docs/engine-and-systems.md new file mode 100644 index 0000000..ed0ad66 --- /dev/null +++ b/docs/engine-and-systems.md @@ -0,0 +1,197 @@ +# Nexus.Engine & Nexus.Systems — Bot Brain + +## BotEngine (Orchestrator) + +The main loop. Owns systems, navigation, profiles, and action execution. + +### Logic Loop (25Hz, background thread) + +``` +1. Wait for MemoryPoller to provide latest GameState +2. CheckCharacterProfile() → auto-load profile if character changed +3. GameStateEnricher.Enrich() → compute NearestEnemies, ThreatMap, DangerLevel +4. Clear ActionQueue +5. NavigationController.Update(state) → compute path, set DesiredDirection +6. Run all ISystem implementations in priority order +7. NavigationSystem submits MoveAction if DesiredDirection is set +8. ActionQueue.Resolve() → merge conflicts +9. ExecuteActions() → emit key/mouse/click commands via IInputController +``` + +### Action Execution + +| Action | Execution | +|--------|-----------| +| MoveAction | Direction → MovementKeyTracker → WASD key state changes (delta-based) | +| CastAction | SmoothMoveTo target + key press. Re-projects moving entities. Adds ±30-50px jitter. | +| FlaskAction | Direct key press | +| KeyAction | Press/Down/Up operations | +| ClickAction | Left/Right/Middle click at screen position | + +### MovementKeyTracker + +Converts world-space direction vectors to WASD keys for isometric camera: + +``` +1. Rotate direction 45° to align with isometric axes +2. sx = dir.X * cos(45°) - dir.Y * sin(45°) +3. sy = dir.X * sin(45°) + dir.Y * cos(45°) +4. W if sy > 0.3, S if sy < -0.3, D if sx > 0.3, A if sx < -0.3 +5. Only emit key changes (delta-based — no redundant KeyDown/KeyUp) +``` + +### Mouse Drift (Navigation) + +During navigation, lazily repositions the mouse toward enemy clusters: +- Projects enemy centroid ahead of player movement +- Applies ±25° angular offset for organic appearance +- Fires every 800-1500ms (randomized) + +### Safety + +- Releases all held keys when loading screen or escape menu detected +- CombatSystem's `ReleaseAllHeld()` called on state transitions + +--- + +## Systems + +### ThreatSystem (Priority 50) + +Emergency threat response. Runs first, only acts on elevated danger. + +| Danger | Response | +|--------|----------| +| Safe / Low | No action | +| Medium | No action (MovementSystem handles soft avoidance) | +| High | Flee toward safety (priority 50, allows casting) | +| Critical or point-blank (<150 units) | **Urgent flee (priority 5) — blocks all casting** | + +**Flee direction**: `Player.Position - ThreatCentroid`, validated against terrain via `FindWalkableDirection()`. + +### MovementSystem (Priority 100) + +Continuous soft avoidance via **inverse-square repulsion field**. + +For each hostile monster within SafeDistance (400 units): +``` +force += (playerPos - enemyPos) / distanceSquared * RepulsionWeight +``` +Normalizes sum, validates against terrain, submits as lower-priority MoveAction. + +Effect: Player gently drifts away from enemies without hard fleeing. + +### AreaProgressionSystem (Priority 199) + +High-level area traversal. Runs before NavigationSystem to take precedence. + +**State machine (7 phases):** +``` +Exploring → Looting → NavigatingToChest → InteractingChest → +NavigatingToTransition → Interacting → TalkingToNpc +``` + +**Exploration strategy:** +1. Check for elite enemies (Rare/Unique within 800u) → yield to combat +2. Check for quest chests → navigate and interact +3. Check for loot (if danger ≤ Low) → pick up within 600u +4. Once fully explored → find area transition matching quest target +5. In towns with active quest → talk to NPC + +**Quest integration**: Queries active quests for target areas. Prioritizes tracked quests, then lowest act, then shortest path. Blacklists failed transitions after 5s timeout. + +**Navigation delegation**: Uses `NavigationController.NavigateToEntity()` and `.Explore()`. Sets targets and yields until reached. + +### NavigationSystem (Priority 200) + +Ultra-thin passthrough. If `NavigationController.DesiredDirection` is set, submits a MoveAction. All actual pathfinding logic lives in NavigationController (see [pathfinding.md](pathfinding.md)). + +### CombatSystem (Priority 300) + +Skill rotation and target selection. Hot-swappable via CharacterProfile. + +**Rotation loop:** +``` +1. Check global cooldown (skip if recently cast) +2. For each skill in priority order: + a. Check per-skill cooldown + b. Match skill to memory via slot index (fallback to name) + c. If aura: cast once per zone + d. If damage: find target → submit CastAction +3. Release held keys for skills without valid targets +``` + +**Target selection pipeline:** +``` +1. Filter by TargetSelection (Nearest, Rarest, MagicPlus, RarePlus, UniqueOnly) +2. Filter by range (SkillProfile.RangeMin/RangeMax) +3. Filter by line-of-sight (terrain query) +4. Check MinMonstersInRange (AOE threshold) +5. Pick best: Rarest mode → prefer higher rarity then nearer; others → nearest +6. Project to screen coordinates +``` + +**Skill input types:** +- LeftClick/RightClick/MiddleClick: Direct click at target position +- KeyPress with MaintainPressed: Hold key continuously +- KeyPress normal: Single tap + +**Kiting/orbit (during global cooldown):** +- Computes enemy centroid +- Moves perpendicular to centroid (orbital movement) +- Applies radial bias to maintain ideal distance +- Flips orbit direction if terrain blocks path +- Persists orbit sign across ticks for smooth motion + +**Cooldown management:** +- Per-skill: `max(skill.CooldownMs, globalCd + 50)` for rotation +- MaintainPressed skills: use skill.CooldownMs directly +- Area reset: clears aura tracking, resets orbit + +### ResourceSystem (Priority 400) + +Flask automation based on life/mana thresholds. + +``` +if LifePercent < LifeFlaskThreshold (50%) && cooldown expired → FlaskAction +if ManaPercent < ManaFlaskThreshold (50%) && cooldown expired → FlaskAction +``` + +Flask cooldown: 4000ms default. Hot-swappable on character profile change. + +### LootSystem (Priority 500) + +Stub — disabled by default. Item pickup logic handled by AreaProgressionSystem's looting phase. + +--- + +## System Interaction Diagram + +``` +GameState (read-only, shared) + │ + ├─→ ThreatSystem ──→ MoveAction (priority 5 or 50) + │ [blocks casting if priority ≤ 10] + │ + ├─→ MovementSystem ──→ MoveAction (priority 100) + │ [soft repulsion, overridable] + │ + ├─→ AreaProgressionSystem ──→ NavigateTo/Explore commands + │ [drives NavigationController] + │ + ├─→ NavigationSystem ──→ MoveAction (priority 200) + │ [passthrough from NavigationController] + │ + ├─→ CombatSystem ──→ CastAction (priority 300) + │ [skill rotation + target selection] + │ + ├─→ ResourceSystem ──→ FlaskAction (priority 400) + │ [always passes through] + │ + └─→ ActionQueue.Resolve() + │ + ├── Highest MoveAction wins + ├── CastAction passes unless blocked by urgent flee + ├── FlaskAction always passes + └──→ ExecuteActions() → IInputController +``` diff --git a/docs/infrastructure.md b/docs/infrastructure.md new file mode 100644 index 0000000..9ced350 --- /dev/null +++ b/docs/infrastructure.md @@ -0,0 +1,207 @@ +# Infrastructure & Game Interaction Projects + +## Nexus.GameOffsets — Memory Layout Definitions + +Pure offset structs for POE2 game memory. No logic, no reading — just struct layouts. + +**Contents:** +- **Entities/**: `EntityStruct`, `EntityDetails`, `ComponentLookup`, `ComponentNameAndIndex`, `ItemStruct`, `EntityTreeNode` +- **Components/** (22 structs): Actor, Animated, Buffs, Chest, Life, Mods, Player, Positioned, Render, Stats, Targetable, Transitionable, WorldItem, etc. +- **States/**: `InGameState`, `AreaInstance`, `AreaLoading`, `ServerData`, `WorldData`, `Inventory`, `ImportantUiElements` +- **Natives/**: C++ STL memory layouts — `StdVector`, `StdMap`, `StdList`, `StdBucket`, `StdWString`, `StdTuple` + +**Key offsets:** +- Actor skills: 0xB00 (ActiveSkillsVector), 0xB18 (CooldownsVector) +- UIElement: 0x10 (Children), 0x98 (StringId), 0x180 (Flags), 0x448 (Text) +- Entity: 0x80 (ID), 0x84 (Flags), 0x08 (EntityDetails) + +**Dependencies**: None. Used by Memory and Data layers. + +--- + +## Nexus.Screen — Screen Capture, OCR & Detection + +Screen capture, OCR, image processing, grid/item detection, loot label detection. + +### Core Components + +| Class | Purpose | +|-------|---------| +| ScreenReader | Main facade — OCR, template matching, screenshot, diff OCR | +| IScreenCapture | Desktop duplication or GDI capture | +| IOcrEngine | Interface for OCR backends (Win native, EasyOCR, OneOCR, WinOCR) | +| PythonOcrBridge | Calls Python script via subprocess for EasyOCR/YOLO | + +### Grid & Item Detection + +| Class | Purpose | +|-------|---------| +| GridReader | Reads stash/inventory grids (12-col 70×70px or 24-col 35×35px) | +| GridHandler | Template matching for cell occupancy, item size detection | +| TemplateMatchHandler | NCC-based visual matching (find identical items in grid) | +| DetectGridHandler | Edge detection to find grid boundaries | + +### Detection Systems + +| Class | Purpose | +|-------|---------| +| EnemyDetector | YOLO/ONNX object detection for enemy positions | +| BossDetector | Boss-specific recognition | +| HudReader | HUD element OCR (HP bar, mana, buffs) | +| GameStateDetector | Main menu vs in-game state | +| ScreenReader.DetectLootLabels() | Three-pass loot detection (polygon, contour, yellow text) | + +### Frame Pipeline + +Pub-sub for screen frames: `FramePipeline` distributes captured frames to multiple `IFrameConsumer` implementations (GameState, Enemy, Boss detectors, Minimap, Navigation). + +**Used by**: Bot, Navigation, Inventory, Ui + +--- + +## Nexus.Game — Game Interaction + +Low-level game control — window focus, input sending, clipboard operations. + +| Class | Purpose | +|-------|---------| +| GameController | Main facade — focus, chat, input, shortcuts | +| InputSender | Win32 SendInput (scan codes), Bézier mouse movement, Ctrl+click | +| WindowManager | SetForegroundWindow (with alt-key trick), GetWindowRect | +| ClipboardHelper | System clipboard read/write | + +**Key operations:** +- `FocusWindow()` — SetForegroundWindow + alt-key trick (required for background processes) +- `CtrlRightClick()` — buying from seller stash +- `MoveMouse()` — Bézier curve smooth move +- `MoveMouseInstant()` — direct teleport (no interpolation) +- `TypeText()`, `SelectAll()`, `Paste()` — clipboard operations + +**Used by**: Inventory, Trade, Items, Navigation, Bot + +--- + +## Nexus.Log — Game Log Watcher + +Parses Client.txt game log at 200ms poll intervals. + +| Event | Pattern | +|-------|---------| +| AreaEntered | `[SCENE] Set Source [AreaName]` or `You have entered AreaName` | +| WhisperReceived | Incoming whisper messages | +| WhisperSent | Outgoing whisper messages | +| TradeAccepted | Trade completion | +| PartyJoined/Left | Party state changes | +| LineReceived | Raw log lines | + +`CurrentArea` detected from log tail on startup. Used by Bot (reset navigation on area change), Inventory (wait for area transitions), Navigation. + +--- + +## Nexus.Trade — Trade Daemon IPC + +Manages trade search monitoring via external Node.js Playwright daemon. + +### TradeDaemonBridge + +Spawns `node tools/trade-daemon/daemon.mjs`, communicates via stdin/stdout JSON. + +**Commands (→ daemon):** +- `start`, `addSearch`, `addDiamondSearch` +- `pauseSearch`, `clickTravel` +- `openScrapPage`, `reloadScrapPage`, `closeScrapPage` + +**Events (← daemon):** +- `newListings` → `NewListings(searchId, items[])` +- `diamondListings` → `DiamondListings(searchId, pricedItems[])` +- `wsClose` → websocket disconnection + +**Trade flow**: Website "Travel to Hideout" button → stash opens → Ctrl+right-click to buy → `/hideout` to go home → store items + +--- + +## Nexus.Items — Item Parsing + +Parse item text from clipboard (Ctrl+C) using Sidekick item parser library. + +| Class | Purpose | +|-------|---------| +| ItemReader | Move to item → Ctrl+C → read clipboard → parse | +| SidekickBootstrapper | Initialize Sidekick parser on first use | + +**Used by**: Bot (identify items during scraping) + +--- + +## Nexus.Inventory — Stash & Grid Management + +Scan player inventory, track item placement, deposit to stash. + +| Class | Purpose | +|-------|---------| +| InventoryManager | Main interface — scan, deposit, clear | +| InventoryTracker | Cell occupancy matrix + item metadata | +| StashCalibrator | Grid boundary calibration via edge detection | + +**Key operations:** +- `ScanInventory()` → screenshot + grid scan → populate tracker +- `DepositItemsToStash()` → find stash NPC → click items with Shift+Ctrl +- `DepositAllToOpenStash()` → scan → click first occupied → repeat +- `ClearToStash()` → scan → deposit all → return to hideout +- `EnsureAtOwnHideout()` → `/hideout` command if needed + +**Grid calibration (2560×1440):** +- Cell sizes: 70×70px (12-col) or 35×35px (24-col), all 840px wide +- Inventory (12×5): origin (1696, 788) +- Stash 12×12: origin (23, 169) or (23, 216) in folder + +--- + +## Nexus.Navigation — Minimap-Based Movement + +Real-time navigation using minimap image matching + pathfinding. Separate from Nexus.Pathfinding (which is grid-based A*). + +| Class | Purpose | +|-------|---------| +| NavigationExecutor | State machine: Capture → Process → Plan → Move → Stuck | +| MinimapCapture | Frame pipeline consumer — wall color detection, checkpoint detection | +| WorldMap | Position matching via cross-correlation, canvas stitching | +| StuckDetector | No-progress detection | +| WallColorTracker | Learns wall palette from initial spawn | + +**Flow**: Capture minimap → detect position via wall color stitching → pathfind → send WASD keys + +--- + +## Nexus.Bot — Top-Level Orchestration + +Central coordinator that wires everything together. + +| Class | Purpose | +|-------|---------| +| BotOrchestrator | DI container, state machine, frame pipeline management | +| TradeExecutor | Single trade flow (navigate → buy → deposit) | +| MappingExecutor | Map exploration (navigate + loot) | +| KulemakExecutor | Boss fight with arena mechanics | +| CraftingExecutor | Crafting bench operations | +| DiamondExecutor | Diamond trade handling | +| ScrapExecutor | Vendor scrapping | +| TradeQueue | FIFO queue of trade tasks | +| LinkManager | Trade search management | + +**Bot modes**: Trading, Mapping, Crafting (via BotMode enum) + +--- + +## Nexus.Ui — Avalonia Desktop Application + +Entry point executable. Avalonia 11.2 + CommunityToolkit.MVVM + FluentTheme. + +**App.xaml.cs** wires all DI: +- Services: ConfigStore, GameController, ScreenReader, ClientLogWatcher, TradeMonitor, InventoryManager +- Bot: FramePipelineService, LinkManager, TradeExecutor, TradeQueue, BotOrchestrator, ModPoolService +- ViewModels: Main, Debug, Settings, Mapping, Atlas, Crafting, Memory, Nexus, ObjectBrowser + +**Additional dependencies**: Vortice.Direct2D1 (overlay rendering), Microsoft.Extensions.DependencyInjection + +**Views**: MainWindow, DebugWindow, SettingsWindow, MappingWindow, etc. with MVVM bindings. diff --git a/docs/input.md b/docs/input.md new file mode 100644 index 0000000..fa8ccb5 --- /dev/null +++ b/docs/input.md @@ -0,0 +1,67 @@ +# Nexus.Input — Input Controllers & Humanization + +## IInputController Implementations + +### SendInputController (Default, No Driver) + +Uses Win32 `SendInput` API with **KEYEVENTF_SCANCODE** flag. Games read hardware scan codes, not virtual key codes. + +- **KeyDown/KeyUp**: Raw keyboard scan code via SendInput struct +- **KeyPress**: Down → Sleep(holdMs) → Up with humanization +- **SmoothMoveTo**: Cubic Bézier curve interpolation (10-40 steps) with random perpendicular spread +- **MouseMoveTo**: Direct `SetCursorPos()` (instant teleport) +- **Clicks**: Smooth move to target → humanized delay → click + +### InterceptionInputController (Driver-Based) + +Uses Interception keyboard/mouse driver for lower-level control: +- Delegates to `KeyboardHook` and `MouseHook` via InputInterceptor COM library +- Same smooth movement and humanization as SendInput +- Returns false from `Initialize()` if driver not installed (graceful fallback) + +### SimInputController (Simulator) + +Implements `IInputController` but doesn't make Win32 calls. Instead: +- **WASD** → Tracks held state, converts to direction vector with 45° isometric rotation +- **Skills** → Queues skill casts to SimWorld via `QueueSkill()` +- **Mouse** → Tracks screen position, converts to world coords via inverse camera matrix +- **Visualization** → Maintains flash timers (0.15s) for InputOverlayRenderer + +## Scan Codes + +``` +Movement: W=0x11 A=0x1E S=0x1F D=0x20 +Skills: Q=0x10 E=0x12 R=0x13 T=0x14 +Numbers: 1=0x02 2=0x03 3=0x04 4=0x05 5=0x06 +Modifiers: LShift=0x2A LCtrl=0x1D LAlt=0x38 +Other: Space=0x39 Enter=0x1C Escape=0x01 Slash=0x35 +``` + +## Humanizer + +Anti-detection layer applied to all input operations. + +| Method | Purpose | +|--------|---------| +| GaussianDelay(baseMs) | Adds gaussian noise (Box-Muller transform), clamped to [50ms, 150ms] | +| JitterPosition(x, y) | Random pixel offset within ClickJitterRadius (3px) | +| ShouldThrottle() | Tracks actions in 60-second rolling window, blocks if APM > MaxApm (250) | +| RecordAction() | Enqueues timestamp for APM tracking | +| RandomizedInterval(baseMs) | Adds ±20% jitter to poll intervals | + +## MovementKeyTracker + +Converts normalized direction vectors to WASD key state for isometric camera: + +``` +Rotate direction 45°: + sx = dir.X * cos(45°) - dir.Y * sin(45°) + sy = dir.X * sin(45°) + dir.Y * cos(45°) + +Key mapping: + W if sy > 0.3, S if sy < -0.3 + D if sx > 0.3, A if sx < -0.3 + +Delta-based: only sends KeyDown/KeyUp when state changes. +Supports holding multiple keys (W+D for diagonal). +``` diff --git a/docs/pathfinding.md b/docs/pathfinding.md new file mode 100644 index 0000000..00bd003 --- /dev/null +++ b/docs/pathfinding.md @@ -0,0 +1,153 @@ +# Nexus.Pathfinding — Navigation & Exploration + +## Overview + +Two classes: **NavigationController** (state machine — decides *where* to go) and **PathFinder** (A* algorithm — decides *how* to get there). + +--- + +## NavigationController + +### Modes + +| Mode | Trigger | Behavior | +|------|---------|----------| +| Idle | `Stop()` | No movement | +| NavigatingToPosition | `NavigateTo(pos)` | Path to fixed world coordinates | +| NavigatingToEntity | `NavigateToEntity(id)` | Chase a moving entity (re-targets each tick) | +| Exploring | `Explore()` | BFS frontier exploration of unmapped terrain | + +### Update Loop (called every tick) + +``` +1. Area change detection → clear path, explored grid, stuck history +2. EnsureExploredGrid() → allocate/resize to match terrain (preserves old data on expansion) +3. MarkExplored(playerPos) → mark cells near player as visited (radius 150 grid cells) +4. ResolveGoal() → get target position based on mode +5. If no goal and Exploring → PickExploreTarget() via BFS +6. Reach detection → within WaypointReachedDistance (80u), clear goal or stop +7. Stuck detection → if < 30u movement in 60 ticks (~1s), repath or pick new target +8. Pathfinding → A* from player to goal (with explored grid bias in explore mode) +9. Waypoint advancement → advance index as player reaches each waypoint +10. Output → DesiredDirection (normalized vector to next waypoint) +``` + +### Explored Grid + +Parallel bool array matching terrain dimensions. Tracks which cells the player has visited. + +- **Mark radius**: 150 grid cells (~1630 world units) — circular region around player +- **Preservation**: On terrain expansion, overlapping explored data is copied to new grid +- **Offset-aware**: Uses same OffsetX/OffsetY as terrain for absolute grid coordinates + +### BFS Exploration (PickExploreTarget) + +When Exploring mode needs a new goal: + +1. **BFS frontier search** (up to 100,000 iterations) + - 8-directional BFS outward from player + - Finds nearest unexplored walkable cell + - Returns that cell as world coordinates + +2. **Random distant target** (if BFS finds nothing) + - 20 attempts at random directions, 1500-3500 world units away + - Pushes player toward terrain edges where expansion triggers + +3. **Edge fallback** (if random fails) + - Heads toward nearest terrain boundary (10 cells from edge) + - Guarantees continued exploration with infinite terrain + +4. **Exploration complete** (only if all fallbacks fail) + - Sets `IsExplorationComplete = true` + - Prevents expensive re-BFS every tick + - Reset on area change + +### Stuck Detection + +- **Window**: Last 60 positions (~1 second at 60Hz) +- **Threshold**: Must move at least 30 world units in window +- **Grace period**: 120 ticks (2 seconds) after picking new explore target +- **On stuck while exploring**: Mark failed goal as explored, pick new target, set grace period +- **On stuck otherwise**: Repath + +### Path Failure Handling + +- **Explored bias fallback**: If A* with explored grid bias fails, retry without bias (bias can make distant targets unreachable) +- **Cooldown**: 3 seconds before retrying after path failure (prevents CPU burn on impossible paths) + +--- + +## PathFinder — A* Implementation + +### Signature + +```csharp +public static List? FindPath( + WalkabilitySnapshot terrain, Vector2 start, Vector2 goal, float worldToGrid, + bool[]? exploredGrid, int exploredWidth, int exploredHeight, + int exploredOffsetX, int exploredOffsetY) +``` + +Returns world-coordinate waypoints or null if unreachable. + +### Movement Model + +- **8-directional grid**: Cardinal + diagonal +- **Costs**: Cardinal = 1.0, Diagonal = √2 ≈ 1.414 +- **Explored penalty**: ×1.5 multiplier for explored cells (biases paths through unexplored territory) + +### Heuristic + +``` +h = max(dx, dy) + 0.414 * min(dx, dy) +``` +Diagonal/Chebyshev-based. Admissible and consistent. + +### Algorithm + +1. **Snap to walkable**: If start/goal in wall, BFS search for nearest walkable cell (radius up to 20) +2. **A* search** (budget: 200,000 iterations): + - Priority queue ordered by f = g + h + - 8 neighbors per expansion + - **Corner-cut check**: Diagonals require at least one adjacent cardinal cell walkable + - **Explored grid bias**: Multiply step cost by 1.5 for explored cells + - Track `bestNode` (closest reachable) for fallback +3. **Path reconstruction**: Backtrack via cameFrom map +4. **Simplification**: Remove collinear waypoints, keep only turning points +5. **Fallback**: If goal unreachable but bestNode is meaningfully closer (within 80% of starting heuristic), path to closest reachable cell + +### Data Structures + +| Structure | Type | Purpose | +|-----------|------|---------| +| Open set | PriorityQueue<(int,int), float> | Nodes to expand, ordered by f-score | +| Closed set | HashSet<(int,int)> | Already expanded nodes | +| gScore | Dictionary<(int,int), float> | Best known cost to each node | +| cameFrom | Dictionary<(int,int), (int,int)> | Backtracking map | + +--- + +## Integration + +``` +AreaProgressionSystem + │ .Explore() / .NavigateTo() / .NavigateToEntity() + ▼ +NavigationController + │ .Update(GameState) → computes path, sets DesiredDirection + │ calls PathFinder.FindPath() for A* routing + ▼ +NavigationSystem + │ reads DesiredDirection → submits MoveAction + ▼ +ActionQueue → BotEngine → MovementKeyTracker → WASD keys +``` + +### Coordinate Systems + +| Space | Example | Conversion | +|-------|---------|------------| +| World | (1517, 4491) | Raw game units | +| Grid | (139, 413) | world × WorldToGrid (23/250) | +| Local grid | (139-ox, 413-oy) | grid - terrain offset | +| Screen | project via CameraMatrix | WorldToScreen.Project() | diff --git a/docs/simulator.md b/docs/simulator.md new file mode 100644 index 0000000..28a6d8c --- /dev/null +++ b/docs/simulator.md @@ -0,0 +1,180 @@ +# Nexus.Simulator — Standalone Game World + +## Purpose + +Test bot systems (combat, navigation, threat assessment) without the real game. Replaces the memory-reading pipeline with a procedural game world. Bot systems run unmodified — they see identical `GameState` objects and emit actions to `IInputController`. + +## Architecture + +``` +SimWorld (game tick loop) + │ + ├── SimPoller (60Hz background thread) + │ ├── FlushToWorld() → transfer input to SimWorld + │ ├── Tick(dt) → advance simulation + │ ├── SimStateBuilder.Build() → SimWorld → GameState + │ └── Push to GameDataCache + │ + ├── SimInputController (captures bot actions) + │ ├── WASD → MoveDirection vector (45° isometric conversion) + │ ├── Skills → QueueSkill(scanCode, targetWorldPos) + │ ├── Mouse → track position, screen↔world conversion + │ └── Flash timers for input visualization + │ + ├── Bot Logic Thread (60Hz) + │ ├── GameStateEnricher.Enrich(state) + │ ├── All 6 systems: Threat, Movement, Navigation, Combat, Resource, Loot + │ ├── NavigationController.Update() + │ └── ExecuteActions() → SimInputController + │ + └── Render Thread (ImGui + Veldrid) + ├── TerrainRenderer (diamond cells, isometric) + ├── EntityRenderer (player, enemies, health bars) + ├── EffectRenderer (melee cones, AOE circles, projectile lines) + ├── PathRenderer (A* waypoints) + ├── InputOverlayRenderer (keyboard + mouse state) + └── DebugPanel (system toggles, stats, spawn controls) +``` + +## SimWorld — Game Loop + +### Tick (dt-based, 60Hz) + +``` +1. CheckAndExpandTerrain() → expand when player within 50 cells of edge +2. MovePlayer(dt) → WASD direction × speed × dt, collision with terrain +3. ProcessSkills() → dequeue skill casts, dispatch by scan code +4. UpdateProjectiles(dt) → move, check terrain/enemy collisions +5. UpdateEffects(dt) → decay visual effects (0.3s duration) +6. UpdateEnemies(dt) → AI state machine per enemy +7. UpdateRespawns(dt) → cull far enemies, spawn new groups +``` + +### Terrain + +- Procedural: all walkable with scattered obstacles (rock clusters, wall segments, pillars) +- 500×500 initial grid, `WorldToGrid = 23/250` +- **Infinite expansion**: Expands 250 cells per side when player within 50 cells of edge +- Preserves existing data via array copy with offset adjustment + +### Player + +- Position (Vector2), Health/Mana with regen (5 HP/s, 10 MP/s) +- Move speed: 400 world units/s +- Collision: slide-along-X / slide-along-Y fallback if direct move blocked + +### Skills + +| Scan Code | Type | Behavior | +|-----------|------|----------| +| Q (0x10), R (0x13) | AOE | Damage all enemies within 250u of target position | +| E (0x12), T (0x14) | Projectile | Spawn projectile, 1200 speed, 800 range, 80u hit radius | +| LMB, RMB | Melee | 150u cone, 120° angle from player toward target | + +Base damage: 200 per hit. Configurable via SimConfig. + +### Enemy AI + +``` +State machine per SimEnemy: + +Idle → wander randomly within 200u of spawn, new target every 2-5s + │ player within 600u (aggro range) + ▼ +Chasing → move toward player at 75% player speed + │ player within 100u (attack range) + ▼ +Attacking → stand still, deal 30 damage every 1.5s + │ player escapes attack range + ▼ back to Chasing + │ health ≤ 0 + ▼ +Dead → visible for 2s → queue for respawn (5s delay) +``` + +### Enemy Spawning + +- **Groups**: 3-7 enemies per spawn, leader keeps rolled rarity, rest are Normal +- **Rarity distribution**: 70% Normal, 20% Magic, 8% Rare, 2% Unique +- **HP multipliers**: Magic=1.5×, Rare=3×, Unique=5× base (200) +- **Spawn ring**: 800-2000 world units from player +- **Direction bias**: ±90° cone ahead of player's movement direction +- **Culling**: Remove enemies > 3000u from player +- **Population**: Maintain 25 enemies, spawn new groups as needed + +## Bridge Layer + +### SimPoller + +Replaces MemoryPoller. Background thread at 60Hz: +1. `FlushToWorld()` — transfer accumulated input +2. `world.Tick(dt)` — advance simulation (dt clamped to 0.1s max) +3. `SimStateBuilder.Build()` — convert to GameState +4. Push to GameDataCache (same fields as production) + +### SimStateBuilder + +Converts SimWorld state → GameState: +- Each SimEnemy → EntitySnapshot (with rarity, threat level, AI state, HP) +- SimPlayer → PlayerState (position, vitals, skills) +- Camera matrix: orthographic projection (12800×7200 world units → 2560×1440 screen) + +### SimInputController + +Implements IInputController, captures actions instead of sending Win32 input: +- WASD → direction vector (with 45° isometric inversion) +- Skills → `SimWorld.QueueSkill(scanCode, worldPos)` +- Mouse → screen position tracking, inverse camera transform for world coords +- Input visualization: flash timers for keyboard/mouse overlay + +## Rendering + +### ViewTransform (Isometric Camera) + +45° counter-clockwise rotation matching the game's camera: + +``` +World → Grid: gx = worldX × WorldToGrid +Grid → Screen: rx = (gx - gy) × cos(45°) + ry = -(gx + gy) × cos(45°) +Screen = canvasOrigin + viewOffset + (rx, ry) × zoom +``` + +### Renderers + +| Renderer | Draws | +|----------|-------| +| TerrainRenderer | Diamond cells (rotated grid), explored overlay, minimap | +| EntityRenderer | Player (green circle), enemies (colored by rarity), health/mana bars | +| EffectRenderer | Melee cones (red triangle fan), AOE circles (blue), projectile lines (cyan) | +| PathRenderer | Cyan waypoint lines and dots from A* path | +| InputOverlayRenderer | Keyboard (3 rows: 1-5, QWERT, ASDF) + mouse (L/R/M buttons) | +| DebugPanel | Pause/speed, player stats, enemy counts, system toggles, threat info | + +### VeldridImGuiRenderer + +Custom ImGui backend for Veldrid 4.9.0 + D3D11: +- HLSL shaders compiled at runtime via D3DCompiler P/Invoke +- Dynamic vertex/index buffers, font texture from ImGui atlas +- Alpha blending pipeline with scissor rect support + +## SimConfig + +``` +Terrain: 500×500, WorldToGrid=23/250, ExpandThreshold=50, ExpandAmount=250 +Player: Speed=400, HP=1000, MP=500, HPRegen=5/s, MPRegen=10/s +Enemies: Count=25, Aggro=600u, Attack=100u, Speed=75%, HP=200, Damage=30 +Spawning: Ring=800-2000u, Groups=3-7, Cull=3000u +Skills: Melee=150u/120°, AOE=250u, Projectile=1200speed/800range, Damage=200 +Rarity: Normal=70%, Magic=20%, Rare=8%, Unique=2% +Simulation: SpeedMultiplier=1×, Pauseable +``` + +## Running + +``` +dotnet run --project src/Nexus.Simulator +``` + +Dependencies: Core, Data, Systems, Pathfinding, ImGui.NET, Veldrid, Veldrid.StartupUtilities +Does NOT depend on: Memory, Input, Screen, Game, Bot, Ui, Trade diff --git a/docs/test.html b/docs/test.html new file mode 100644 index 0000000..87f1b56 --- /dev/null +++ b/docs/test.html @@ -0,0 +1,95 @@ + + + + + + Hold Timer + + + +
+ +
Last hold: 0 ms
+
Current hold: 0 ms
+
+ + + + \ No newline at end of file diff --git a/imgui.ini b/imgui.ini index 6e55fd4..039b318 100644 --- a/imgui.ini +++ b/imgui.ini @@ -9,7 +9,7 @@ Size=219,425 Collapsed=0 [Window][Simulator] -Pos=11,220 +Pos=341,232 Size=1200,681 Collapsed=0 diff --git a/src/Nexus.Core/ActionExecutor.cs b/src/Nexus.Core/ActionExecutor.cs new file mode 100644 index 0000000..11b3ec6 --- /dev/null +++ b/src/Nexus.Core/ActionExecutor.cs @@ -0,0 +1,52 @@ +using System.Numerics; + +namespace Nexus.Core; + +public static class ActionExecutor +{ + public static void Execute(List resolved, IInputController input, + MovementKeyTracker moveTracker, MovementBlender blender, Vector2? playerPos = null) + { + if (!input.IsInitialized) return; + + // Discrete actions + foreach (var action in resolved) + { + switch (action) + { + case FlaskAction flask: + input.KeyPress(flask.FlaskScanCode); + break; + + case CastAction cast: + if (cast.TargetScreenPos.HasValue) + input.SmoothMoveTo((int)cast.TargetScreenPos.Value.X, (int)cast.TargetScreenPos.Value.Y); + input.KeyPress(cast.SkillScanCode); + break; + + case ClickAction click: + var cx = (int)click.ScreenPosition.X; + var cy = (int)click.ScreenPosition.Y; + switch (click.Type) + { + case ClickType.Left: input.LeftClick(cx, cy); break; + case ClickType.Right: input.RightClick(cx, cy); break; + case ClickType.Middle: input.MiddleClick(cx, cy); break; + } + break; + + case KeyAction key: + switch (key.Type) + { + case KeyActionType.Press: input.KeyPress(key.ScanCode); break; + case KeyActionType.Down: input.KeyDown(key.ScanCode); break; + case KeyActionType.Up: input.KeyUp(key.ScanCode); break; + } + break; + } + } + + // WASD movement (delta-based held keys) + moveTracker.Apply(input, blender.Direction, playerPos); + } +} diff --git a/src/Nexus.Core/ActionQueue.cs b/src/Nexus.Core/ActionQueue.cs index 377c87b..997b4fd 100644 --- a/src/Nexus.Core/ActionQueue.cs +++ b/src/Nexus.Core/ActionQueue.cs @@ -33,12 +33,11 @@ public class ActionQueue } /// - /// Resolve conflicts and return the final action list: + /// Resolve conflicts and return the final action list. + /// Movement is handled by MovementBlender — only non-movement actions remain here. /// 1. FlaskActions always pass through - /// 2. Get highest-priority MoveAction + CastAction - /// 3. Urgent move (priority ≤ 10) → include move, BLOCK cast (flee) - /// 4. Normal → include both cast + move - /// 5. All other actions pass through + /// 2. Get highest-priority CastAction + /// 3. All other actions pass through /// public List Resolve() { @@ -51,21 +50,9 @@ public class ActionQueue resolved.Add(action); } - var bestMove = GetHighestPriority(); var bestCast = GetHighestPriority(); - - if (bestMove is not null) - { - resolved.Add(bestMove); - - // Urgent flee (priority ≤ 10) blocks casting - if (bestMove.Priority > 10 && bestCast is not null) - resolved.Add(bestCast); - } - else if (bestCast is not null) - { + if (bestCast is not null) resolved.Add(bestCast); - } // Pass through everything else (Key, Click, Chat, Wait) except types already handled foreach (var action in _actions) diff --git a/src/Nexus.Core/BotConfig.cs b/src/Nexus.Core/BotConfig.cs index e06fe25..eac93b7 100644 --- a/src/Nexus.Core/BotConfig.cs +++ b/src/Nexus.Core/BotConfig.cs @@ -7,8 +7,8 @@ public class BotConfig public int MemoryPollRateHz { get; set; } = 30; // Movement - public float SafeDistance { get; set; } = 400f; - public float RepulsionWeight { get; set; } = 1.5f; + public float SafeDistance { get; set; } = 500f; + public float RepulsionWeight { get; set; } = 0.5f; public float WaypointReachedDistance { get; set; } = 80f; // Navigation diff --git a/src/Nexus.Core/ISystem.cs b/src/Nexus.Core/ISystem.cs index e904755..7b03dbe 100644 --- a/src/Nexus.Core/ISystem.cs +++ b/src/Nexus.Core/ISystem.cs @@ -5,5 +5,5 @@ public interface ISystem int Priority { get; } string Name { get; } bool IsEnabled { get; set; } - void Update(GameState state, ActionQueue actions); + void Update(GameState state, ActionQueue actions, MovementBlender movement); } diff --git a/src/Nexus.Core/Logging.cs b/src/Nexus.Core/Logging.cs index 742b887..67465d0 100644 --- a/src/Nexus.Core/Logging.cs +++ b/src/Nexus.Core/Logging.cs @@ -8,7 +8,7 @@ public static class Logging public static void Setup() { Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() + .MinimumLevel.Debug() .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.File("logs/poe2trade-.log", diff --git a/src/Nexus.Core/MovementBlender.cs b/src/Nexus.Core/MovementBlender.cs new file mode 100644 index 0000000..0b25de0 --- /dev/null +++ b/src/Nexus.Core/MovementBlender.cs @@ -0,0 +1,231 @@ +using System.Numerics; + +namespace Nexus.Core; + +public readonly record struct MovementIntent( + int Layer, + Vector2 Direction, + float Override = 0f, + string? Source = null +); + +/// +/// Blends movement contributions from multiple systems using priority-layered attenuation. +/// Higher-priority layers (lower number) attenuate lower-priority ones via their Override factor. +/// Applies terrain validation once on the blended result. WASD hysteresis handles smoothing. +/// +public sealed class MovementBlender +{ + private readonly List _intents = new(); + + // Stuck detection + private Vector2 _lastResolvePos; + private int _stuckFrames; + private const int StuckFrameThreshold = 30; // ~0.5s at 60Hz + private const float StuckMovePerFrame = 3f; // must move > 3 world units per frame to count as moving + + // EMA smoothing to dampen terrain validation jitter. + // Snap decision based on INTENT change (pre-terrain), not terrain output — prevents + // terrain probe noise from bypassing the EMA via the snap threshold. + private Vector2? _smoothedDirection; + private Vector2? _lastIntentDir; // pre-terrain direction from previous frame + private const float SmoothingAlpha = 0.12f; // 12% new, 88% previous + + // Terrain validation cache — prevents re-probing within the same grid cell, + // breaking the position↔direction feedback loop that causes zigzag oscillation + private Vector2 _cachedTerrainInputDir; + private Vector2 _cachedTerrainResult; + private int _cachedTerrainGridX = int.MinValue; + private int _cachedTerrainGridY = int.MinValue; + + public Vector2? Direction { get; private set; } + public Vector2? RawDirection { get; private set; } + + /// True when layer 0 (critical flee) was submitted — blocks casting. + public bool IsUrgentFlee { get; private set; } + + /// True when the player hasn't moved for several frames — orbit/herd suppressed. + public bool IsStuck { get; private set; } + + /// Snapshot of intents from the last Resolve() call, for diagnostic logging. + public IReadOnlyList LastIntents => _lastIntents; + private List _lastIntents = new(); + + public void Submit(MovementIntent intent) => _intents.Add(intent); + + /// + /// Clears intents for a new frame. Called at the top of each logic tick. + /// + public void Clear() => _intents.Clear(); + + /// + /// Blends all submitted intents and validates against terrain. + /// Applies EMA smoothing after terrain validation to dampen probe jitter. + /// + public void Resolve(WalkabilitySnapshot? terrain, Vector2 playerPos, float worldToGrid) + { + IsUrgentFlee = false; + + // ── Stuck detection ── + // If player barely moves for ~0.5s, suppress orbit/herd so navigation can guide out + var moved = Vector2.Distance(playerPos, _lastResolvePos); + if (moved < StuckMovePerFrame) + _stuckFrames++; + else + _stuckFrames = Math.Max(0, _stuckFrames - 3); // recover 3x faster than building up + _lastResolvePos = playerPos; + IsStuck = _stuckFrames > StuckFrameThreshold; + + if (IsStuck) + { + // Keep only flee (L0, L1) and navigation (L3) — drop orbit (L2) and herd (L4) + _intents.RemoveAll(i => i.Layer == 2 || i.Layer == 4); + } + + _lastIntents = new List(_intents); + + if (_intents.Count == 0) + { + RawDirection = null; + Direction = null; + return; + } + + // Check for urgent flee (layer 0) + foreach (var intent in _intents) + { + if (intent.Layer == 0) + { + IsUrgentFlee = true; + break; + } + } + + // Group by layer, sum within layer, track max override per layer + var layers = new SortedDictionary(); + foreach (var intent in _intents) + { + if (layers.TryGetValue(intent.Layer, out var existing)) + layers[intent.Layer] = (existing.Sum + intent.Direction, Math.Max(existing.MaxOverride, intent.Override)); + else + layers[intent.Layer] = (intent.Direction, intent.Override); + } + + // Blend across layers with priority-based attenuation + var attenuation = 1f; + var result = Vector2.Zero; + + foreach (var (_, (sum, maxOverride)) in layers) + { + result += sum * attenuation; + attenuation *= (1f - maxOverride); + } + + if (result.LengthSquared() < 0.0001f) + { + RawDirection = null; + Direction = null; + return; + } + + // Normalize the blended result + var rawDir = Vector2.Normalize(result); + var intentDir = rawDir; // save pre-terrain direction for snap decision + + // Terrain validation with grid-cell caching. + // Re-probe only when the raw direction changes (>~14°) or the player enters a new grid cell. + // This prevents the feedback loop: direction jitter → zigzag movement → crosses cell boundary → more jitter. + if (terrain is not null) + { + var gx = (int)(playerPos.X * worldToGrid); + var gy = (int)(playerPos.Y * worldToGrid); + var dirSimilar = Vector2.Dot(rawDir, _cachedTerrainInputDir) > 0.97f; + var sameCell = gx == _cachedTerrainGridX && gy == _cachedTerrainGridY; + + if (dirSimilar && sameCell) + { + rawDir = _cachedTerrainResult; + } + else + { + var preTerrainDir = rawDir; + rawDir = TerrainQuery.FindWalkableDirection(terrain, playerPos, rawDir, worldToGrid); + _cachedTerrainInputDir = preTerrainDir; + _cachedTerrainResult = rawDir; + _cachedTerrainGridX = gx; + _cachedTerrainGridY = gy; + } + } + + RawDirection = rawDir; + + // EMA smoothing. Snap decision based on whether the INTENT (pre-terrain) changed, + // not the terrain output. This prevents terrain probe noise (which can produce 90°+ swings) + // from bypassing the EMA via the snap threshold. + if (_smoothedDirection.HasValue) + { + var intentChanged = _lastIntentDir.HasValue && + Vector2.Dot(_lastIntentDir.Value, intentDir) < 0f; + + if (intentChanged) + { + // Genuine intent reversal (flee, new waypoint) — snap immediately + } + else + { + // Intent is stable — all direction change is terrain noise, always smooth + var smoothed = Vector2.Lerp(_smoothedDirection.Value, rawDir, SmoothingAlpha); + if (smoothed.LengthSquared() > 0.0001f) + rawDir = Vector2.Normalize(smoothed); + } + } + + _smoothedDirection = rawDir; + _lastIntentDir = intentDir; + Direction = rawDir; + } + + /// + /// Full reset — call on area change or loading screen. + /// + public void Reset() + { + _intents.Clear(); + _lastIntents.Clear(); + Direction = null; + RawDirection = null; + IsUrgentFlee = false; + IsStuck = false; + _stuckFrames = 0; + _lastResolvePos = Vector2.Zero; + _smoothedDirection = null; + _lastIntentDir = null; + _cachedTerrainGridX = int.MinValue; + _cachedTerrainGridY = int.MinValue; + } + + /// + /// Compact diagnostic string: lists active intents and final direction. + /// Example: "Orbit(L2,0.0) Navigation(L3,0.0) Herd(L4,0.2) → (0.71,-0.31)" + /// + public string DiagnosticSummary() + { + if (_lastIntents.Count == 0) + return "none"; + + var parts = new List(); + foreach (var intent in _lastIntents) + { + var dir = intent.Direction; + var mag = dir.Length(); + parts.Add($"{intent.Source ?? "?"}(L{intent.Layer},ovr={intent.Override:F1},mag={mag:F2})"); + } + + var dirStr = Direction.HasValue + ? $"({Direction.Value.X:F2},{Direction.Value.Y:F2})" + : "null"; + + var stuckStr = IsStuck ? " [STUCK]" : ""; + return string.Join(" + ", parts) + " → " + dirStr + stuckStr; + } +} diff --git a/src/Nexus.Core/MovementKeyTracker.cs b/src/Nexus.Core/MovementKeyTracker.cs new file mode 100644 index 0000000..3f56c64 --- /dev/null +++ b/src/Nexus.Core/MovementKeyTracker.cs @@ -0,0 +1,122 @@ +using System.Numerics; +using Serilog; + +namespace Nexus.Core; + +/// +/// Translates a movement direction vector into WASD key presses. +/// Applies 45° rotation to account for isometric camera (W+A = one world axis). +/// Tracks which keys are currently held and only sends changes (delta). +/// Enforces a minimum hold duration (55±10ms gaussian) on every key press. +/// +public sealed class MovementKeyTracker +{ + private bool _wHeld, _aHeld, _sHeld, _dHeld; + private long _wDownAt, _aDownAt, _sDownAt, _dDownAt; + private int _wMinHold, _aMinHold, _sMinHold, _dMinHold; + private Vector2? _lastPlayerPos; + + private static readonly Random Rng = new(); + + // 45° rotation constants + private const float Cos45 = 0.70710678f; + private const float Sin45 = 0.70710678f; + + // Hysteresis: higher threshold to press, lower to release — prevents oscillation + private const float PressThreshold = 0.35f; + private const float ReleaseThreshold = 0.15f; + + /// + /// Apply a movement direction. Null or zero direction releases all keys. + /// Direction is in world space; we rotate 45° for the isometric camera before mapping to WASD. + /// Uses hysteresis to prevent key oscillation. + /// + public void Apply(IInputController input, Vector2? direction, Vector2? playerPos = null) + { + _lastPlayerPos = playerPos; + + bool wantW, wantA, wantS, wantD; + + if (direction is { } dir && dir.LengthSquared() > 0.001f) + { + // Rotate 45° for isometric camera alignment + var sx = dir.X * Cos45 - dir.Y * Sin45; + var sy = dir.X * Sin45 + dir.Y * Cos45; + + // Hysteresis: different thresholds for press vs release + wantW = _wHeld ? sy > ReleaseThreshold : sy > PressThreshold; + wantS = _sHeld ? sy < -ReleaseThreshold : sy < -PressThreshold; + wantD = _dHeld ? sx > ReleaseThreshold : sx > PressThreshold; + wantA = _aHeld ? sx < -ReleaseThreshold : sx < -PressThreshold; + } + else + { + wantW = wantA = wantS = wantD = false; + } + + var now = Environment.TickCount64; + SetKey(input, ScanCodes.W, ref _wHeld, ref _wDownAt, ref _wMinHold, wantW, now, _lastPlayerPos); + SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, wantA, now, _lastPlayerPos); + SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, wantS, now, _lastPlayerPos); + SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, wantD, now, _lastPlayerPos); + } + + /// + /// Release all movement keys immediately (bypasses min hold — for shutdown/area change). + /// + public void ReleaseAll(IInputController input) + { + if (_wHeld) { input.KeyUp(ScanCodes.W); _wHeld = false; } + if (_aHeld) { input.KeyUp(ScanCodes.A); _aHeld = false; } + if (_sHeld) { input.KeyUp(ScanCodes.S); _sHeld = false; } + if (_dHeld) { input.KeyUp(ScanCodes.D); _dHeld = false; } + } + + private static string KeyName(ushort scanCode) => scanCode switch + { + 0x11 => "W", 0x1E => "A", 0x1F => "S", 0x20 => "D", _ => $"0x{scanCode:X2}" + }; + + private static void SetKey(IInputController input, ushort scanCode, + ref bool held, ref long downAt, ref int minHold, bool want, long now, Vector2? pos) + { + if (want && !held) + { + input.KeyDown(scanCode); + held = true; + downAt = now; + minHold = HoldMs(); + if (pos.HasValue) + Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms) pos=({X:F0},{Y:F0})", + KeyName(scanCode), minHold, pos.Value.X, pos.Value.Y); + else + Log.Information("[WASD] {Key} DOWN (minHold={MinHold}ms)", KeyName(scanCode), minHold); + } + else if (!want && held) + { + var elapsed = now - downAt; + if (elapsed < minHold) return; // enforce minimum hold + input.KeyUp(scanCode); + held = false; + if (pos.HasValue) + Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms) pos=({X:F0},{Y:F0})", + KeyName(scanCode), elapsed, minHold, pos.Value.X, pos.Value.Y); + else + Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms)", KeyName(scanCode), elapsed, minHold); + } + } + + /// Gaussian hold duration peaked at 55ms, range [44, 76]. + private static int HoldMs() + { + double u, v, s; + do + { + u = Rng.NextDouble() * 2.0 - 1.0; + v = Rng.NextDouble() * 2.0 - 1.0; + s = u * u + v * v; + } while (s >= 1.0 || s == 0.0); + var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s); + return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76); + } +} diff --git a/src/Nexus.Core/ScanCodes.cs b/src/Nexus.Core/ScanCodes.cs new file mode 100644 index 0000000..16c1069 --- /dev/null +++ b/src/Nexus.Core/ScanCodes.cs @@ -0,0 +1,56 @@ +namespace Nexus.Core; + +/// +/// Hardware scan codes for keyboard input. +/// +public static class ScanCodes +{ + // WASD movement + public const ushort W = 0x11; + public const ushort A = 0x1E; + public const ushort S = 0x1F; + public const ushort D = 0x20; + + // Number row + public const ushort Key1 = 0x02; + public const ushort Key2 = 0x03; + public const ushort Key3 = 0x04; + public const ushort Key4 = 0x05; + public const ushort Key5 = 0x06; + public const ushort Key6 = 0x07; + public const ushort Key7 = 0x08; + public const ushort Key8 = 0x09; + public const ushort Key9 = 0x0A; + public const ushort Key0 = 0x0B; + + // Modifiers + public const ushort LShift = 0x2A; + public const ushort RShift = 0x36; + public const ushort LCtrl = 0x1D; + public const ushort LAlt = 0x38; + + // Common keys + public const ushort Escape = 0x01; + public const ushort Tab = 0x0F; + public const ushort Space = 0x39; + public const ushort Enter = 0x1C; + public const ushort Backspace = 0x0E; + + // Function keys + public const ushort F1 = 0x3B; + public const ushort F2 = 0x3C; + public const ushort F3 = 0x3D; + public const ushort F4 = 0x3E; + public const ushort F5 = 0x3F; + + // Letters (commonly used) + public const ushort Q = 0x10; + public const ushort E = 0x12; + public const ushort R = 0x13; + public const ushort T = 0x14; + public const ushort I = 0x17; + public const ushort F = 0x21; + + // Slash (for chat commands like /hideout) + public const ushort Slash = 0x35; +} diff --git a/src/Nexus.Core/TerrainQuery.cs b/src/Nexus.Core/TerrainQuery.cs index 87ed1da..e6237fc 100644 --- a/src/Nexus.Core/TerrainQuery.cs +++ b/src/Nexus.Core/TerrainQuery.cs @@ -46,7 +46,7 @@ public static class TerrainQuery ///
public static Vector2 FindWalkableDirection( WalkabilitySnapshot terrain, Vector2 playerPos, Vector2 desiredDir, float worldToGrid, - float probeDistance = 200f) + float probeDistance = 60f) { if (IsDirectionClear(terrain, playerPos, desiredDir, worldToGrid, probeDistance)) return desiredDir; @@ -67,15 +67,59 @@ public static class TerrainQuery private static bool IsDirectionClear( WalkabilitySnapshot terrain, Vector2 origin, Vector2 dir, float worldToGrid, float distance) { - var endpoint = origin + dir * distance; + // Check near (2-3 grid cells), mid, and far probes + var nearpoint = origin + dir * 30f; var midpoint = origin + dir * (distance * 0.5f); + var endpoint = origin + dir * distance; + int nx = (int)(nearpoint.X * worldToGrid); + int ny = (int)(nearpoint.Y * worldToGrid); int mx = (int)(midpoint.X * worldToGrid); int my = (int)(midpoint.Y * worldToGrid); int ex = (int)(endpoint.X * worldToGrid); int ey = (int)(endpoint.Y * worldToGrid); - return terrain.IsWalkable(mx, my) && terrain.IsWalkable(ex, ey); + return terrain.IsWalkable(nx, ny) && terrain.IsWalkable(mx, my) && terrain.IsWalkable(ex, ey); + } + + /// + /// Probes 8 directions around the player for nearby walls. + /// Returns a normalized push-away vector, or Zero if no walls are close. + /// + public static Vector2 ComputeWallRepulsion(WalkabilitySnapshot terrain, Vector2 playerPos, float worldToGrid) + { + const float probeNear = 25f; // ~2-3 grid cells + const float probeFar = 60f; // ~5-6 grid cells + + var push = Vector2.Zero; + + for (var i = 0; i < 8; i++) + { + var angle = i * MathF.PI / 4f; + var dir = new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + + // Near probe — strong push + var near = playerPos + dir * probeNear; + var nx = (int)(near.X * worldToGrid); + var ny = (int)(near.Y * worldToGrid); + if (!terrain.IsWalkable(nx, ny)) + { + push -= dir * 1.0f; + continue; // don't double-count + } + + // Far probe — gentle push + var far = playerPos + dir * probeFar; + var fx = (int)(far.X * worldToGrid); + var fy = (int)(far.Y * worldToGrid); + if (!terrain.IsWalkable(fx, fy)) + push -= dir * 0.4f; + } + + if (push.LengthSquared() < 0.0001f) + return Vector2.Zero; + + return Vector2.Normalize(push); } private static Vector2 Rotate(Vector2 v, float degrees) diff --git a/src/Nexus.Core/WalkabilitySnapshot.cs b/src/Nexus.Core/WalkabilitySnapshot.cs index cb1fecc..f3acd7a 100644 --- a/src/Nexus.Core/WalkabilitySnapshot.cs +++ b/src/Nexus.Core/WalkabilitySnapshot.cs @@ -6,10 +6,23 @@ public record WalkabilitySnapshot public int Height { get; init; } public byte[] Data { get; init; } = []; - public bool IsWalkable(int x, int y) + /// + /// Absolute grid X coordinate of the top-left corner of Data. + /// Grid coord gx maps to local index gx - OffsetX. + /// + public int OffsetX { get; init; } + + /// + /// Absolute grid Y coordinate of the top-left corner of Data. + /// + public int OffsetY { get; init; } + + public bool IsWalkable(int gx, int gy) { - if (x < 0 || x >= Width || y < 0 || y >= Height) + var lx = gx - OffsetX; + var ly = gy - OffsetY; + if (lx < 0 || lx >= Width || ly < 0 || ly >= Height) return false; - return Data[y * Width + x] != 0; + return Data[ly * Width + lx] != 0; } } diff --git a/src/Nexus.Data/GameStateEnricher.cs b/src/Nexus.Data/GameStateEnricher.cs index 1cb35fb..545dc47 100644 --- a/src/Nexus.Data/GameStateEnricher.cs +++ b/src/Nexus.Data/GameStateEnricher.cs @@ -63,26 +63,50 @@ public static class GameStateEnricher } /// - /// Computes danger using a weighted threat score. + /// Computes danger using effective HP (life + energy shield) and a weighted threat score. /// Close enemies count more, rares/uniques escalate significantly. + /// Hysteresis: de-escalation requires a larger margin than escalation to prevent oscillation. /// + private static DangerLevel _previousDanger = DangerLevel.Safe; + private static float _smoothedThreatScore; + private static long _lastEscalationMs; + private static DangerLevel ComputeDangerLevel(GameState state) { - if (state.Player.LifePercent < 30f) return DangerLevel.Critical; - if (state.Player.LifePercent < 50f) return DangerLevel.High; + // Effective HP = life + ES combined + var effectiveHp = state.Player.LifeCurrent + state.Player.EsCurrent; + var effectiveMax = state.Player.LifeTotal + state.Player.EsTotal; + var effectivePercent = effectiveMax > 0 ? (float)effectiveHp / effectiveMax * 100f : 0f; - // Weighted threat score: proximity × rarity multiplier + // Pure life check — if ES is gone and life is low, it's critical (no hysteresis) + if (state.Player.LifePercent < 25f) + { + _previousDanger = DangerLevel.Critical; + return DangerLevel.Critical; + } + if (effectivePercent < 35f) + { + _previousDanger = DangerLevel.Critical; + return DangerLevel.Critical; + } + if (effectivePercent < 50f) + { + var hpLevel = DangerLevel.High; + if (hpLevel < _previousDanger) + hpLevel = _previousDanger; // don't de-escalate from HP alone + _previousDanger = hpLevel; + return hpLevel; + } + + // Weighted threat score: smooth distance falloff × rarity multiplier var threatScore = 0f; foreach (var m in state.HostileMonsters) { var d = m.DistanceToPlayer; if (d > 800f) continue; - // Distance weight: closer = more dangerous - float distWeight; - if (d < 200f) distWeight = 3f; - else if (d < 400f) distWeight = 2f; - else distWeight = 1f; + // Smooth distance weight: linear falloff from 3.0 at d=0 to 0.5 at d=800 + var distWeight = 3f - 2.5f * (d / 800f); // Rarity multiplier var rarityMul = m.Rarity switch @@ -96,10 +120,48 @@ public static class GameStateEnricher threatScore += distWeight * rarityMul; } - if (threatScore >= 15f) return DangerLevel.Critical; - if (threatScore >= 8f) return DangerLevel.High; - if (threatScore >= 4f) return DangerLevel.Medium; - if (threatScore > 0f) return DangerLevel.Low; - return DangerLevel.Safe; + // EMA smoothing — prevents single-frame score spikes from causing oscillation. + // Snap upward (escalation is instant), smooth downward (de-escalation is gradual). + const float deescalationAlpha = 0.08f; + if (threatScore >= _smoothedThreatScore) + _smoothedThreatScore = threatScore; // snap up — react instantly to new threats + else + _smoothedThreatScore += (threatScore - _smoothedThreatScore) * deescalationAlpha; + threatScore = _smoothedThreatScore; + + // Escalation thresholds + var level = DangerLevel.Safe; + if (threatScore >= 15f) level = DangerLevel.Critical; + else if (threatScore >= 8f) level = DangerLevel.High; + else if (threatScore >= 4f) level = DangerLevel.Medium; + else if (threatScore > 0f) level = DangerLevel.Low; + + // Hysteresis: minimum hold time + score margins prevent oscillation + var now = Environment.TickCount64; + if (level != _previousDanger) + { + // Hold any level for at least 1.5 seconds before allowing ANY transition + if (now - _lastEscalationMs < 1500) + { + level = _previousDanger; + } + else if (level < _previousDanger) + { + // Score-based hysteresis — only drop one level at a time + if (_previousDanger >= DangerLevel.Critical) + level = DangerLevel.High; + else if (_previousDanger >= DangerLevel.High) + level = DangerLevel.Medium; + else if (_previousDanger >= DangerLevel.Medium && threatScore >= 2f) + level = DangerLevel.Medium; + } + } + + // Track any transition + if (level != _previousDanger) + _lastEscalationMs = now; + + _previousDanger = level; + return level; } } diff --git a/src/Nexus.Input/InterceptionInputController.cs b/src/Nexus.Input/InterceptionInputController.cs index 12dfcae..33bbde4 100644 --- a/src/Nexus.Input/InterceptionInputController.cs +++ b/src/Nexus.Input/InterceptionInputController.cs @@ -73,10 +73,12 @@ public sealed partial class InterceptionInputController : IInputController, IDis if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; - holdMs = _humanizer.GaussianDelay(holdMs); _humanizer.RecordAction(); } - _keyboard?.SimulateKeyPress((KeyCode)scanCode, holdMs); + var hold = HoldMs(); + Log.Information("[Key] 0x{ScanCode:X2} DOWN (hold={HoldMs}ms)", scanCode, hold); + _keyboard?.SimulateKeyPress((KeyCode)scanCode, hold); + Log.Information("[Key] 0x{ScanCode:X2} UP", scanCode); } public void MouseMoveTo(int x, int y) @@ -150,7 +152,10 @@ public sealed partial class InterceptionInputController : IInputController, IDis } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - _mouse?.SimulateLeftButtonClick(_humanizer?.GaussianDelay(50) ?? 50); + var hold = HoldMs(); + Log.Information("[Click] Left DOWN (hold={HoldMs}ms)", hold); + _mouse?.SimulateLeftButtonClick(hold); + Log.Information("[Click] Left UP"); } public void RightClick(int x, int y) @@ -164,7 +169,10 @@ public sealed partial class InterceptionInputController : IInputController, IDis } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - _mouse?.SimulateRightButtonClick(_humanizer?.GaussianDelay(50) ?? 50); + var hold = HoldMs(); + Log.Information("[Click] Right DOWN (hold={HoldMs}ms)", hold); + _mouse?.SimulateRightButtonClick(hold); + Log.Information("[Click] Right UP"); } public void MiddleClick(int x, int y) @@ -178,7 +186,24 @@ public sealed partial class InterceptionInputController : IInputController, IDis } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - _mouse?.SimulateMiddleButtonClick(_humanizer?.GaussianDelay(50) ?? 50); + var hold = HoldMs(); + Log.Information("[Click] Middle DOWN (hold={HoldMs}ms)", hold); + _mouse?.SimulateMiddleButtonClick(hold); + Log.Information("[Click] Middle UP"); + } + + /// Gaussian hold duration peaked at 55ms, range [44, 76]. + private static int HoldMs() + { + double u, v, s; + do + { + u = Rng.NextDouble() * 2.0 - 1.0; + v = Rng.NextDouble() * 2.0 - 1.0; + s = u * u + v * v; + } while (s >= 1.0 || s == 0.0); + var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s); + return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76); } public void LeftDown() diff --git a/src/Nexus.Input/ScanCodes.cs b/src/Nexus.Input/ScanCodes.cs index 959b878..aa2dbf9 100644 --- a/src/Nexus.Input/ScanCodes.cs +++ b/src/Nexus.Input/ScanCodes.cs @@ -1,56 +1,44 @@ -namespace Nexus.Input; - -/// -/// Hardware scan codes for keyboard input via Interception driver. -/// -public static class ScanCodes +// ScanCodes has moved to Nexus.Core. This file re-exports for backward compatibility. +// ReSharper disable once CheckNamespace +namespace Nexus.Input { - // WASD movement - public const ushort W = 0x11; - public const ushort A = 0x1E; - public const ushort S = 0x1F; - public const ushort D = 0x20; - - // Number row - public const ushort Key1 = 0x02; - public const ushort Key2 = 0x03; - public const ushort Key3 = 0x04; - public const ushort Key4 = 0x05; - public const ushort Key5 = 0x06; - public const ushort Key6 = 0x07; - public const ushort Key7 = 0x08; - public const ushort Key8 = 0x09; - public const ushort Key9 = 0x0A; - public const ushort Key0 = 0x0B; - - // Modifiers - public const ushort LShift = 0x2A; - public const ushort RShift = 0x36; - public const ushort LCtrl = 0x1D; - public const ushort LAlt = 0x38; - - // Common keys - public const ushort Escape = 0x01; - public const ushort Tab = 0x0F; - public const ushort Space = 0x39; - public const ushort Enter = 0x1C; - public const ushort Backspace = 0x0E; - - // Function keys - public const ushort F1 = 0x3B; - public const ushort F2 = 0x3C; - public const ushort F3 = 0x3D; - public const ushort F4 = 0x3E; - public const ushort F5 = 0x3F; - - // Letters (commonly used) - public const ushort Q = 0x10; - public const ushort E = 0x12; - public const ushort R = 0x13; - public const ushort T = 0x14; - public const ushort I = 0x17; - public const ushort F = 0x21; - - // Slash (for chat commands like /hideout) - public const ushort Slash = 0x35; + /// Re-exports Nexus.Core.ScanCodes for backward compatibility. + public static class ScanCodes + { + public const ushort W = Core.ScanCodes.W; + public const ushort A = Core.ScanCodes.A; + public const ushort S = Core.ScanCodes.S; + public const ushort D = Core.ScanCodes.D; + public const ushort Key1 = Core.ScanCodes.Key1; + public const ushort Key2 = Core.ScanCodes.Key2; + public const ushort Key3 = Core.ScanCodes.Key3; + public const ushort Key4 = Core.ScanCodes.Key4; + public const ushort Key5 = Core.ScanCodes.Key5; + public const ushort Key6 = Core.ScanCodes.Key6; + public const ushort Key7 = Core.ScanCodes.Key7; + public const ushort Key8 = Core.ScanCodes.Key8; + public const ushort Key9 = Core.ScanCodes.Key9; + public const ushort Key0 = Core.ScanCodes.Key0; + public const ushort LShift = Core.ScanCodes.LShift; + public const ushort RShift = Core.ScanCodes.RShift; + public const ushort LCtrl = Core.ScanCodes.LCtrl; + public const ushort LAlt = Core.ScanCodes.LAlt; + public const ushort Escape = Core.ScanCodes.Escape; + public const ushort Tab = Core.ScanCodes.Tab; + public const ushort Space = Core.ScanCodes.Space; + public const ushort Enter = Core.ScanCodes.Enter; + public const ushort Backspace = Core.ScanCodes.Backspace; + public const ushort F1 = Core.ScanCodes.F1; + public const ushort F2 = Core.ScanCodes.F2; + public const ushort F3 = Core.ScanCodes.F3; + public const ushort F4 = Core.ScanCodes.F4; + public const ushort F5 = Core.ScanCodes.F5; + public const ushort Q = Core.ScanCodes.Q; + public const ushort E = Core.ScanCodes.E; + public const ushort R = Core.ScanCodes.R; + public const ushort T = Core.ScanCodes.T; + public const ushort I = Core.ScanCodes.I; + public const ushort F = Core.ScanCodes.F; + public const ushort Slash = Core.ScanCodes.Slash; + } } diff --git a/src/Nexus.Input/SendInputController.cs b/src/Nexus.Input/SendInputController.cs index 8e4ed9d..b9b3873 100644 --- a/src/Nexus.Input/SendInputController.cs +++ b/src/Nexus.Input/SendInputController.cs @@ -46,13 +46,15 @@ public sealed partial class SendInputController : IInputController if (_humanizer is not null) { if (_humanizer.ShouldThrottle()) return; - holdMs = _humanizer.GaussianDelay(holdMs); _humanizer.RecordAction(); } + var hold = HoldMs(); + Log.Information("[Key] 0x{ScanCode:X2} DOWN (hold={HoldMs}ms)", scanCode, hold); KeyDown(scanCode); - Thread.Sleep(holdMs); + Thread.Sleep(hold); KeyUp(scanCode); + Log.Information("[Key] 0x{ScanCode:X2} UP", scanCode); } // ── Mouse movement ── @@ -113,7 +115,7 @@ public sealed partial class SendInputController : IInputController } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - MouseClick(MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, _humanizer?.GaussianDelay(50) ?? 50); + MouseClick(MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, HoldMs()); } public void RightClick(int x, int y) @@ -127,7 +129,7 @@ public sealed partial class SendInputController : IInputController } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - MouseClick(MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, _humanizer?.GaussianDelay(50) ?? 50); + MouseClick(MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP, HoldMs()); } public void MiddleClick(int x, int y) @@ -141,7 +143,7 @@ public sealed partial class SendInputController : IInputController } SmoothMoveTo(x, y); Thread.Sleep(_humanizer is not null ? _humanizer.GaussianDelay(10) : 10); - MouseClick(MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, _humanizer?.GaussianDelay(50) ?? 50); + MouseClick(MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, HoldMs()); } public void LeftDown() @@ -170,13 +172,38 @@ public sealed partial class SendInputController : IInputController // ── Private helpers ── + /// Gaussian hold duration peaked at 55ms, range [44, 76]. + private static int HoldMs() + { + double u, v, s; + do + { + u = Rng.NextDouble() * 2.0 - 1.0; + v = Rng.NextDouble() * 2.0 - 1.0; + s = u * u + v * v; + } while (s >= 1.0 || s == 0.0); + var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s); + return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76); + } + + private static string ClickName(uint downFlag) => downFlag switch + { + MOUSEEVENTF_LEFTDOWN => "Left", + MOUSEEVENTF_RIGHTDOWN => "Right", + MOUSEEVENTF_MIDDLEDOWN => "Middle", + _ => "?" + }; + private void MouseClick(uint downFlag, uint upFlag, int holdMs) { + var name = ClickName(downFlag); + Log.Information("[Click] {Button} DOWN (hold={HoldMs}ms)", name, holdMs); var down = MakeMouseInput(downFlag); var up = MakeMouseInput(upFlag); SendInput(1, [down], INPUT_SIZE); Thread.Sleep(holdMs); SendInput(1, [up], INPUT_SIZE); + Log.Information("[Click] {Button} UP", name); } private static double EaseInOutQuad(double t) => diff --git a/src/Nexus.Pathfinding/NavigationController.cs b/src/Nexus.Pathfinding/NavigationController.cs index 831d587..b24f646 100644 --- a/src/Nexus.Pathfinding/NavigationController.cs +++ b/src/Nexus.Pathfinding/NavigationController.cs @@ -28,16 +28,20 @@ public sealed class NavigationController // Explored grid — tracks which terrain cells the player has visited private bool[]? _exploredGrid; private int _exploredWidth, _exploredHeight; + private int _exploredOffsetX, _exploredOffsetY; private const int ExploreMarkRadius = 150; // grid cells (~1630 world units) // Stuck detection: rolling window of recent positions private readonly Queue _positionHistory = new(); - private const int StuckWindowSize = 10; - private const float StuckThreshold = 5f; + private const int StuckWindowSize = 120; // ~2 seconds at 60Hz + private const float StuckThreshold = 50f; // must move at least 50 world units in that window // Path failure cooldown — don't retry immediately when pathfinding fails private long _pathFailCooldownMs; + // Grace period after picking a new explore target — don't check stuck immediately + private int _stuckGraceTicks; + public NavMode Mode { get; private set; } = NavMode.Idle; public Vector2? DesiredDirection { get; private set; } public IReadOnlyList? CurrentPath => _path; @@ -45,6 +49,8 @@ public sealed class NavigationController public bool[]? ExploredGrid => _exploredGrid; public int ExploredWidth => _exploredWidth; public int ExploredHeight => _exploredHeight; + public int ExploredOffsetX => _exploredOffsetX; + public int ExploredOffsetY => _exploredOffsetY; /// /// True when BFS exploration finds no more unexplored walkable cells in the current area. @@ -136,39 +142,14 @@ public sealed class NavigationController IsExplorationComplete = false; } - // Allocate explored grid on first tick with terrain, after area change, - // or when terrain dimensions change (prevents bounds mismatch crash) + // Allocate or resize explored grid to match terrain (preserving old data on expansion) var terrain = state.Terrain; - if (terrain is not null && - (_exploredGrid is null || terrain.Width != _exploredWidth || terrain.Height != _exploredHeight)) - { - _exploredWidth = terrain.Width; - _exploredHeight = terrain.Height; - _exploredGrid = new bool[_exploredWidth * _exploredHeight]; - } + if (terrain is not null) + EnsureExploredGrid(terrain); // Mark cells near player as explored if (_exploredGrid is not null && terrain is not null) - { - var pgx = (int)(playerPos.X * _config.WorldToGrid); - var pgy = (int)(playerPos.Y * _config.WorldToGrid); - var r = ExploreMarkRadius; - var r2 = r * r; - var minX = Math.Max(0, pgx - r); - var maxX = Math.Min(_exploredWidth - 1, pgx + r); - var minY = Math.Max(0, pgy - r); - var maxY = Math.Min(_exploredHeight - 1, pgy + r); - for (var y = minY; y <= maxY; y++) - { - var dy = y - pgy; - for (var x = minX; x <= maxX; x++) - { - var dx = x - pgx; - if (dx * dx + dy * dy <= r2) - _exploredGrid[y * _exploredWidth + x] = true; - } - } - } + MarkExplored(playerPos); // Resolve goal based on mode var goal = ResolveGoal(state); @@ -211,8 +192,11 @@ public sealed class NavigationController if (_positionHistory.Count > StuckWindowSize) _positionHistory.Dequeue(); + if (_stuckGraceTicks > 0) + _stuckGraceTicks--; + var isStuck = false; - if (_positionHistory.Count >= StuckWindowSize && _path is not null) + if (_stuckGraceTicks <= 0 && _positionHistory.Count >= StuckWindowSize && _path is not null) { var oldest = _positionHistory.Peek(); if (Vector2.Distance(oldest, playerPos) < StuckThreshold) @@ -221,10 +205,16 @@ public sealed class NavigationController if (Mode == NavMode.Exploring) { Log.Information("NavigationController: stuck while exploring, picking new target"); + + // Mark cells around the failed goal as explored so BFS won't pick the same target + if (_goalPosition.HasValue && _exploredGrid is not null) + MarkExplored(_goalPosition.Value); + _goalPosition = null; _path = null; _waypointIndex = 0; _positionHistory.Clear(); + _stuckGraceTicks = 120; // 2 seconds grace for next target return; } Log.Debug("NavigationController: stuck detected, repathing"); @@ -264,7 +254,7 @@ public sealed class NavigationController _path = Mode == NavMode.Exploring ? PathFinder.FindPath(terrain, playerPos, goal.Value, _config.WorldToGrid, - _exploredGrid, _exploredWidth, _exploredHeight) + _exploredGrid, _exploredWidth, _exploredHeight, _exploredOffsetX, _exploredOffsetY) : PathFinder.FindPath(terrain, playerPos, goal.Value, _config.WorldToGrid); _waypointIndex = 0; _pathTimestampMs = now; @@ -321,18 +311,54 @@ public sealed class NavigationController // Diagnostic: log every ~60 ticks (once per second at 60Hz) if (state.TickNumber % 60 == 0) { - var gx = (int)(playerPos.X * _config.WorldToGrid); - var gy = (int)(playerPos.Y * _config.WorldToGrid); - var walkable = state.Terrain?.IsWalkable(gx, gy) ?? false; + var gx2 = (int)(playerPos.X * _config.WorldToGrid); + var gy2 = (int)(playerPos.Y * _config.WorldToGrid); + var walkable = state.Terrain?.IsWalkable(gx2, gy2) ?? false; Log.Information( "NAV DIAG: playerWorld=({Px:F0},{Py:F0}) playerGrid=({Gx},{Gy}) walkable={W} " + "waypointWorld=({Tx:F0},{Ty:F0}) dir=({Dx:F2},{Dy:F2})", - playerPos.X, playerPos.Y, gx, gy, walkable, + playerPos.X, playerPos.Y, gx2, gy2, walkable, target.X, target.Y, DesiredDirection.Value.X, DesiredDirection.Value.Y); } } + private void EnsureExploredGrid(WalkabilitySnapshot terrain) + { + var needsResize = _exploredGrid is null + || terrain.Width != _exploredWidth || terrain.Height != _exploredHeight + || terrain.OffsetX != _exploredOffsetX || terrain.OffsetY != _exploredOffsetY; + + if (!needsResize) return; + + var newGrid = new bool[terrain.Width * terrain.Height]; + + // Preserve old explored data in overlapping region + if (_exploredGrid is not null) + { + var overlapMinX = Math.Max(terrain.OffsetX, _exploredOffsetX); + var overlapMinY = Math.Max(terrain.OffsetY, _exploredOffsetY); + var overlapMaxX = Math.Min(terrain.OffsetX + terrain.Width, _exploredOffsetX + _exploredWidth); + var overlapMaxY = Math.Min(terrain.OffsetY + terrain.Height, _exploredOffsetY + _exploredHeight); + + for (var ay = overlapMinY; ay < overlapMaxY; ay++) + for (var ax = overlapMinX; ax < overlapMaxX; ax++) + { + var oldLx = ax - _exploredOffsetX; + var oldLy = ay - _exploredOffsetY; + var newLx = ax - terrain.OffsetX; + var newLy = ay - terrain.OffsetY; + newGrid[newLy * terrain.Width + newLx] = _exploredGrid[oldLy * _exploredWidth + oldLx]; + } + } + + _exploredGrid = newGrid; + _exploredWidth = terrain.Width; + _exploredHeight = terrain.Height; + _exploredOffsetX = terrain.OffsetX; + _exploredOffsetY = terrain.OffsetY; + } + private Vector2? ResolveGoal(GameState state) { switch (Mode) @@ -365,22 +391,26 @@ public sealed class NavigationController if (state.Terrain is null || _exploredGrid is null) return null; var terrain = state.Terrain; - // Bail if terrain dimensions don't match the allocated grid (area transition in progress) if (terrain.Width != _exploredWidth || terrain.Height != _exploredHeight) return null; var gridToWorld = 1f / _config.WorldToGrid; var playerPos = state.Player.Position; + var ox = terrain.OffsetX; + var oy = terrain.OffsetY; var w = terrain.Width; var h = terrain.Height; - var startGx = Math.Clamp((int)(playerPos.X * _config.WorldToGrid), 0, w - 1); - var startGy = Math.Clamp((int)(playerPos.Y * _config.WorldToGrid), 0, h - 1); + // Player in local grid coords + var pgx = (int)(playerPos.X * _config.WorldToGrid); + var pgy = (int)(playerPos.Y * _config.WorldToGrid); + var startLx = Math.Clamp(pgx - ox, 0, w - 1); + var startLy = Math.Clamp(pgy - oy, 0, h - 1); // BFS outward from player to find nearest unexplored walkable cell var visited = new bool[w * h]; - var queue = new Queue<(int x, int y)>(); - queue.Enqueue((startGx, startGy)); - visited[startGy * w + startGx] = true; + var queue = new Queue<(int lx, int ly)>(); + queue.Enqueue((startLx, startLy)); + visited[startLy * w + startLx] = true; var iterations = 0; const int maxIterations = 100_000; @@ -388,13 +418,15 @@ public sealed class NavigationController while (queue.Count > 0 && iterations++ < maxIterations) { var (cx, cy) = queue.Dequeue(); + var ax = cx + ox; + var ay = cy + oy; // Found an unexplored walkable cell - if (terrain.IsWalkable(cx, cy) && !_exploredGrid[cy * w + cx]) + if (terrain.IsWalkable(ax, ay) && !IsExploredAt(ax, ay)) { - var worldPos = new Vector2(cx * gridToWorld, cy * gridToWorld); + var worldPos = new Vector2(ax * gridToWorld, ay * gridToWorld); _goalPosition = worldPos; - Log.Debug("BFS frontier: target ({Gx},{Gy}) after {Iter} iterations", cx, cy, iterations); + Log.Debug("BFS frontier: target ({Gx},{Gy}) after {Iter} iterations", ax, ay, iterations); return worldPos; } @@ -406,17 +438,101 @@ public sealed class NavigationController if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; var idx = ny * w + nx; if (visited[idx]) continue; - if (!terrain.IsWalkable(nx, ny)) continue; + if (!terrain.IsWalkable(nx + ox, ny + oy)) continue; visited[idx] = true; queue.Enqueue((nx, ny)); } } + // Don't declare exploration complete — with infinite terrain, new cells appear at edges. + // Pick a random distant target to push toward terrain boundaries where expansion triggers. + var randomTarget = PickRandomDistantTarget(playerPos, terrain, gridToWorld); + if (randomTarget is not null) + { + _goalPosition = randomTarget; + Log.Information("BFS frontier: no unexplored cells nearby, roaming to ({X:F0},{Y:F0})", + randomTarget.Value.X, randomTarget.Value.Y); + return randomTarget; + } + Log.Information("BFS frontier: no unexplored cells found — exploration complete"); IsExplorationComplete = true; return null; } + private Vector2? PickRandomDistantTarget(Vector2 playerPos, WalkabilitySnapshot terrain, float gridToWorld) + { + // Try random directions at a moderate distance — aim for terrain edges + for (var attempt = 0; attempt < 20; attempt++) + { + var angle = _rng.NextSingle() * MathF.Tau; + var dist = 1500f + _rng.NextSingle() * 2000f; // 1500-3500 world units away + var target = playerPos + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist; + var gx = (int)(target.X * _config.WorldToGrid); + var gy = (int)(target.Y * _config.WorldToGrid); + if (terrain.IsWalkable(gx, gy)) + return target; + } + + // Fallback: pick a point toward the nearest terrain edge (guaranteed to push toward expansion) + var pgx = (int)(playerPos.X * _config.WorldToGrid); + var pgy = (int)(playerPos.Y * _config.WorldToGrid); + var ox = terrain.OffsetX; + var oy = terrain.OffsetY; + var distToLeft = pgx - ox; + var distToRight = (ox + terrain.Width) - pgx; + var distToTop = pgy - oy; + var distToBottom = (oy + terrain.Height) - pgy; + + // Find the nearest edge and go toward it + var minEdgeDist = Math.Min(Math.Min(distToLeft, distToRight), Math.Min(distToTop, distToBottom)); + int tgx, tgy; + if (minEdgeDist == distToLeft) + { tgx = ox + 10; tgy = pgy; } + else if (minEdgeDist == distToRight) + { tgx = ox + terrain.Width - 10; tgy = pgy; } + else if (minEdgeDist == distToTop) + { tgx = pgx; tgy = oy + 10; } + else + { tgx = pgx; tgy = oy + terrain.Height - 10; } + + if (terrain.IsWalkable(tgx, tgy)) + return new Vector2(tgx * gridToWorld, tgy * gridToWorld); + + return null; + } + + private void MarkExplored(Vector2 worldPos) + { + if (_exploredGrid is null) return; + var gx = (int)(worldPos.X * _config.WorldToGrid); + var gy = (int)(worldPos.Y * _config.WorldToGrid); + var r = ExploreMarkRadius; + var r2 = r * r; + for (var dy = -r; dy <= r; dy++) + for (var dx = -r; dx <= r; dx++) + { + if (dx * dx + dy * dy > r2) continue; + SetExploredAt(gx + dx, gy + dy); + } + } + + private void SetExploredAt(int gx, int gy) + { + var lx = gx - _exploredOffsetX; + var ly = gy - _exploredOffsetY; + if (lx >= 0 && lx < _exploredWidth && ly >= 0 && ly < _exploredHeight) + _exploredGrid![ly * _exploredWidth + lx] = true; + } + + private bool IsExploredAt(int gx, int gy) + { + var lx = gx - _exploredOffsetX; + var ly = gy - _exploredOffsetY; + if (lx < 0 || lx >= _exploredWidth || ly < 0 || ly >= _exploredHeight) return false; + return _exploredGrid![ly * _exploredWidth + lx]; + } + private static readonly int[] _bfsDx = [-1, 0, 1, 0, -1, -1, 1, 1]; private static readonly int[] _bfsDy = [0, -1, 0, 1, -1, 1, -1, 1]; } diff --git a/src/Nexus.Pathfinding/PathFinder.cs b/src/Nexus.Pathfinding/PathFinder.cs index cbc05c9..c29fdf5 100644 --- a/src/Nexus.Pathfinding/PathFinder.cs +++ b/src/Nexus.Pathfinding/PathFinder.cs @@ -12,24 +12,27 @@ public static class PathFinder /// /// A* pathfinding on WalkabilitySnapshot. Returns world-coord waypoints or null if no path. - /// When exploredGrid is provided, explored cells cost 3x more — biasing paths through unexplored territory. + /// When exploredGrid is provided, explored cells cost 1.5x more — biasing paths through unexplored territory. /// public static List? FindPath( WalkabilitySnapshot terrain, Vector2 start, Vector2 goal, float worldToGrid, - bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0) + bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0, + int exploredOffsetX = 0, int exploredOffsetY = 0) { var w = terrain.Width; var h = terrain.Height; + var ox = terrain.OffsetX; + var oy = terrain.OffsetY; var gridToWorld = 1f / worldToGrid; - var startGx = Math.Clamp((int)(start.X * worldToGrid), 0, w - 1); - var startGy = Math.Clamp((int)(start.Y * worldToGrid), 0, h - 1); - var goalGx = Math.Clamp((int)(goal.X * worldToGrid), 0, w - 1); - var goalGy = Math.Clamp((int)(goal.Y * worldToGrid), 0, h - 1); + var startGx = Math.Clamp((int)(start.X * worldToGrid), ox, ox + w - 1); + var startGy = Math.Clamp((int)(start.Y * worldToGrid), oy, oy + h - 1); + var goalGx = Math.Clamp((int)(goal.X * worldToGrid), ox, ox + w - 1); + var goalGy = Math.Clamp((int)(goal.Y * worldToGrid), oy, oy + h - 1); // Snap to nearest walkable if start/goal are in walls - (startGx, startGy) = SnapToWalkable(terrain, startGx, startGy, w, h); - (goalGx, goalGy) = SnapToWalkable(terrain, goalGx, goalGy, w, h); + (startGx, startGy) = SnapToWalkable(terrain, startGx, startGy); + (goalGx, goalGy) = SnapToWalkable(terrain, goalGx, goalGy); var startNode = (startGx, startGy); var goalNode = (goalGx, goalGy); @@ -78,7 +81,7 @@ public static class PathFinder var nx = current.x + Dx[i]; var ny = current.y + Dy[i]; - if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + // IsWalkable handles offset and bounds if (!terrain.IsWalkable(nx, ny)) continue; var neighbor = (nx, ny); @@ -93,8 +96,15 @@ public static class PathFinder } var stepCost = Cost[i]; - if (exploredGrid is not null && nx < exploredWidth && ny < exploredHeight && exploredGrid[ny * exploredWidth + nx]) - stepCost *= 1.5f; + if (exploredGrid is not null) + { + var elx = nx - exploredOffsetX; + var ely = ny - exploredOffsetY; + if (elx >= 0 && elx < exploredWidth && ely >= 0 && ely < exploredHeight + && exploredGrid[ely * exploredWidth + elx]) + stepCost *= 1.5f; + } + var tentativeG = currentG + stepCost; if (tentativeG < gScore.GetValueOrDefault(neighbor, float.MaxValue)) @@ -129,7 +139,7 @@ public static class PathFinder return Math.Max(dx, dy) + 0.414f * Math.Min(dx, dy); } - private static (int, int) SnapToWalkable(WalkabilitySnapshot terrain, int gx, int gy, int w, int h) + private static (int, int) SnapToWalkable(WalkabilitySnapshot terrain, int gx, int gy) { if (terrain.IsWalkable(gx, gy)) return (gx, gy); @@ -143,7 +153,7 @@ public static class PathFinder if (Math.Abs(dx) != r && Math.Abs(dy) != r) continue; var nx = gx + dx; var ny = gy + dy; - if (nx >= 0 && nx < w && ny >= 0 && ny < h && terrain.IsWalkable(nx, ny)) + if (terrain.IsWalkable(nx, ny)) return (nx, ny); } } diff --git a/src/Nexus.Simulator/Bridge/SimInputController.cs b/src/Nexus.Simulator/Bridge/SimInputController.cs index 794c67e..aed2596 100644 --- a/src/Nexus.Simulator/Bridge/SimInputController.cs +++ b/src/Nexus.Simulator/Bridge/SimInputController.cs @@ -1,6 +1,7 @@ using System.Numerics; using Nexus.Core; using Nexus.Simulator.World; +using Serilog; namespace Nexus.Simulator.Bridge; @@ -22,8 +23,27 @@ public class SimInputController : IInputController // Camera matrix for screen→world conversion private Matrix4x4? _cameraMatrix; + // Input visualization tracking + private readonly Dictionary _keyTimers = new(); + private readonly float[] _mouseTimers = new float[3]; + private const float FlashDuration = 0.3f; + + // Smooth mouse interpolation + private Vector2 _mouseMoveStartPos; + private Vector2 _mouseTargetPos; + private float _mouseMoveProgress = 1f; // 1 = arrived + private const float MouseMoveSpeed = 6f; // interpolation speed (higher = faster) + public bool IsInitialized => true; + /// + /// The bot's current mouse position in screen coordinates (for mock cursor rendering). + /// + public Vector2 MouseScreenPos + { + get { lock (_lock) return _mouseScreenPos; } + } + public SimInputController(SimWorld world) { _world = world; @@ -74,6 +94,11 @@ public class SimInputController : IInputController // IInputController implementation — captures actions, no actual Win32 calls + private static string KeyName(ushort sc) => sc switch + { + 0x11 => "W", 0x1E => "A", 0x1F => "S", 0x20 => "D", _ => $"0x{sc:X2}" + }; + public void KeyDown(ushort scanCode) { lock (_lock) @@ -85,6 +110,7 @@ public class SimInputController : IInputController case 0x1F: _sHeld = true; break; // S case 0x20: _dHeld = true; break; // D } + _keyTimers[scanCode] = float.MaxValue; } } @@ -99,11 +125,16 @@ public class SimInputController : IInputController case 0x1F: _sHeld = false; break; case 0x20: _dHeld = false; break; } + _keyTimers.Remove(scanCode); } } public void KeyPress(ushort scanCode, int holdMs = 50) { + lock (_lock) + { + _keyTimers[scanCode] = FlashDuration; + } // Queue as skill cast var target = ScreenToWorld(_mouseScreenPos); _world.QueueSkill(scanCode, target); @@ -114,7 +145,15 @@ public class SimInputController : IInputController lock (_lock) { _mouseScreenPos = new Vector2(x, y); } } - public void SmoothMoveTo(int x, int y) => MouseMoveTo(x, y); + public void SmoothMoveTo(int x, int y) + { + lock (_lock) + { + _mouseMoveStartPos = _mouseScreenPos; + _mouseTargetPos = new Vector2(x, y); + _mouseMoveProgress = 0f; + } + } public void MouseMoveBy(int dx, int dy) { lock (_lock) { _mouseScreenPos += new Vector2(dx, dy); } @@ -122,14 +161,17 @@ public class SimInputController : IInputController public void LeftClick(int x, int y) { + Log.Information("[Click] Left at ({X},{Y})", x, y); + lock (_lock) { _mouseTimers[0] = FlashDuration; } MouseMoveTo(x, y); var target = ScreenToWorld(new Vector2(x, y)); - // LMB = default attack / melee _world.QueueSkill(0, target); } public void RightClick(int x, int y) { + Log.Information("[Click] Right at ({X},{Y})", x, y); + lock (_lock) { _mouseTimers[1] = FlashDuration; } MouseMoveTo(x, y); var target = ScreenToWorld(new Vector2(x, y)); _world.QueueSkill(1, target); @@ -137,13 +179,93 @@ public class SimInputController : IInputController public void MiddleClick(int x, int y) { + Log.Information("[Click] Middle at ({X},{Y})", x, y); + lock (_lock) { _mouseTimers[2] = FlashDuration; } MouseMoveTo(x, y); var target = ScreenToWorld(new Vector2(x, y)); _world.QueueSkill(2, target); } - public void LeftDown() { } - public void LeftUp() { } - public void RightDown() { } - public void RightUp() { } + public void LeftDown() { lock (_lock) { _mouseTimers[0] = float.MaxValue; } } + public void LeftUp() { lock (_lock) { _mouseTimers[0] = 0; } } + public void RightDown() { lock (_lock) { _mouseTimers[1] = float.MaxValue; } } + public void RightUp() { lock (_lock) { _mouseTimers[1] = 0; } } + + /// + /// Decrement flash timers and interpolate mouse. Call once per frame with frame delta time. + /// + public void UpdateTimers(float dt) + { + lock (_lock) + { + // Key flash timers + var expired = new List(); + var updates = new List<(ushort key, float time)>(); + + foreach (var kvp in _keyTimers) + { + if (kvp.Value < 1000f) // not held (held = MaxValue) + { + var t = kvp.Value - dt; + if (t <= 0) expired.Add(kvp.Key); + else updates.Add((kvp.Key, t)); + } + } + + foreach (var k in expired) _keyTimers.Remove(k); + foreach (var (k, t) in updates) _keyTimers[k] = t; + + // Mouse button flash timers + for (var i = 0; i < 3; i++) + if (_mouseTimers[i] > 0 && _mouseTimers[i] < 1000f) + _mouseTimers[i] = MathF.Max(0, _mouseTimers[i] - dt); + + // Smooth mouse interpolation toward target + if (_mouseMoveProgress < 1f) + { + _mouseMoveProgress = MathF.Min(1f, _mouseMoveProgress + dt * MouseMoveSpeed); + // Ease-out: fast start, slow end + var t = 1f - MathF.Pow(1f - _mouseMoveProgress, 3f); + _mouseScreenPos = Vector2.Lerp(_mouseMoveStartPos, _mouseTargetPos, t); + } + } + } + + /// + /// Snapshot of currently active keys and mouse buttons for visualization. + /// + public InputSnapshot GetInputSnapshot() + { + lock (_lock) + { + var keys = new HashSet(); + foreach (var (k, t) in _keyTimers) + if (t > 0) keys.Add(k); + return new InputSnapshot(keys, _mouseTimers[0] > 0, _mouseTimers[1] > 0, _mouseTimers[2] > 0); + } + } +} + +public readonly struct InputSnapshot +{ + private readonly HashSet? _activeKeys; + private readonly bool _leftMouse, _rightMouse, _middleMouse; + + public InputSnapshot(HashSet activeKeys, bool left, bool right, bool middle) + { + _activeKeys = activeKeys; + _leftMouse = left; + _rightMouse = right; + _middleMouse = middle; + } + + public bool IsKeyActive(ushort scanCode) => _activeKeys?.Contains(scanCode) ?? false; + + public bool IsMouseActive(int button) => button switch + { + 0 => _leftMouse, + 1 => _rightMouse, + 2 => _middleMouse, + _ => false, + }; } diff --git a/src/Nexus.Simulator/Bridge/SimPoller.cs b/src/Nexus.Simulator/Bridge/SimPoller.cs index 872c569..d88218f 100644 --- a/src/Nexus.Simulator/Bridge/SimPoller.cs +++ b/src/Nexus.Simulator/Bridge/SimPoller.cs @@ -83,7 +83,8 @@ public sealed class SimPoller : IDisposable _world.Player.Position.X, _world.Player.Position.Y, 0f); _cache.PlayerVitals = new PlayerVitalsData( _world.Player.Health, _world.Player.MaxHealth, - _world.Player.Mana, _world.Player.MaxMana, 0, 0); + _world.Player.Mana, _world.Player.MaxMana, + _world.Player.Es, _world.Player.MaxEs); _cache.IsLoading = false; _cache.IsEscapeOpen = false; _cache.Entities = state.Entities; diff --git a/src/Nexus.Simulator/Bridge/SimStateBuilder.cs b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs index e209499..e441879 100644 --- a/src/Nexus.Simulator/Bridge/SimStateBuilder.cs +++ b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs @@ -71,6 +71,8 @@ public static class SimStateBuilder LifeTotal = player.MaxHealth, ManaCurrent = player.Mana, ManaTotal = player.MaxMana, + EsCurrent = player.Es, + EsTotal = player.MaxEs, Skills = BuildSkillStates(), }, }; diff --git a/src/Nexus.Simulator/Config/SimConfig.cs b/src/Nexus.Simulator/Config/SimConfig.cs index 7231c97..e0c23e0 100644 --- a/src/Nexus.Simulator/Config/SimConfig.cs +++ b/src/Nexus.Simulator/Config/SimConfig.cs @@ -9,25 +9,45 @@ public class SimConfig // Player public float PlayerMoveSpeed { get; set; } = 400f; - public int PlayerMaxHealth { get; set; } = 1000; + public int PlayerMaxHealth { get; set; } = 800; public int PlayerMaxMana { get; set; } = 500; - public float PlayerHealthRegen { get; set; } = 5f; + public int PlayerMaxEs { get; set; } = 400; + public float PlayerHealthRegen { get; set; } = 8f; public float PlayerManaRegen { get; set; } = 10f; + public float PlayerEsRegen { get; set; } = 30f; // ES recharge rate (per second, once recharging) + public float PlayerEsRechargeDelay { get; set; } = 2f; // Seconds after last damage before ES recharges - // Enemies - public int TargetEnemyCount { get; set; } = 25; + // Enemies — melee + public int TargetEnemyCount { get; set; } = 50; public float EnemyAggroRange { get; set; } = 600f; - public float EnemyAttackRange { get; set; } = 100f; + public float EnemyMeleeAttackRange { get; set; } = 100f; public float EnemyMoveSpeedFactor { get; set; } = 0.75f; public int EnemyBaseHealth { get; set; } = 200; - public int EnemyAttackDamage { get; set; } = 30; - public float EnemyAttackCooldown { get; set; } = 1.5f; + public int EnemyMeleeBaseDamage { get; set; } = 60; + public float EnemyMeleeAttackCooldown { get; set; } = 1.2f; + + // Enemies — ranged + public float RangedEnemyChance { get; set; } = 0.30f; // 30% of enemies are ranged + public float EnemyRangedAttackRange { get; set; } = 500f; + public float EnemyRangedPreferredRange { get; set; } = 350f; + public int EnemyRangedBaseDamage { get; set; } = 40; + public float EnemyRangedAttackCooldown { get; set; } = 2.0f; + public float EnemyProjectileSpeed { get; set; } = 800f; + public float EnemyProjectileHitRadius { get; set; } = 40f; + + // Enemies — general public float EnemyDespawnTime { get; set; } = 2f; public float EnemyRespawnTime { get; set; } = 5f; public float EnemyWanderRadius { get; set; } = 200f; + public float EnemySpawnMinDist { get; set; } = 800f; + public float EnemySpawnMaxDist { get; set; } = 2000f; + public float EnemyCullDist { get; set; } = 3000f; + public int EnemyGroupMin { get; set; } = 3; + public int EnemyGroupMax { get; set; } = 7; + public float EnemyGroupSpread { get; set; } = 120f; - // Skills - public float MeleeRange { get; set; } = 150f; + // Player skills + public float MeleeRange { get; set; } = 350f; public float MeleeConeAngle { get; set; } = 120f; public float AoeRadius { get; set; } = 250f; public float ProjectileSpeed { get; set; } = 1200f; @@ -35,6 +55,10 @@ public class SimConfig public float ProjectileHitRadius { get; set; } = 80f; public int SkillBaseDamage { get; set; } = 200; + // Terrain expansion + public int ExpandThreshold { get; set; } = 50; + public int ExpandAmount { get; set; } = 250; + // Simulation public float SpeedMultiplier { get; set; } = 1f; public bool IsPaused { get; set; } diff --git a/src/Nexus.Simulator/Program.cs b/src/Nexus.Simulator/Program.cs index 30330f0..aecf079 100644 --- a/src/Nexus.Simulator/Program.cs +++ b/src/Nexus.Simulator/Program.cs @@ -14,8 +14,8 @@ using Veldrid.Sdl2; using Veldrid.StartupUtilities; Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); Log.Information("Nexus Simulator starting..."); @@ -36,24 +36,7 @@ var poller = new SimPoller(world, input, cache, simConfig); var nav = new NavigationController(botConfig); // ── Create systems (same as BotEngine, minus AreaProgression) ── -var systems = new List -{ - new ThreatSystem { WorldToGrid = botConfig.WorldToGrid }, - new MovementSystem - { - SafeDistance = botConfig.SafeDistance, - RepulsionWeight = botConfig.RepulsionWeight, - WorldToGrid = botConfig.WorldToGrid, - }, - new NavigationSystem - { - WorldToGrid = botConfig.WorldToGrid, - WaypointReachedDistance = botConfig.WaypointReachedDistance, - }, - new CombatSystem(botConfig), - new ResourceSystem(botConfig), - new LootSystem(), -}; +var systems = SystemFactory.CreateSystems(botConfig, nav); // Apply a default profile with configured skills var profile = new CharacterProfile @@ -61,15 +44,15 @@ var profile = new CharacterProfile Name = "SimPlayer", Skills = [ - new() { SlotIndex = 0, Label = "LMB", InputType = SkillInputType.LeftClick, Priority = 0, RangeMax = 150f, RequiresTarget = true }, - new() { SlotIndex = 1, Label = "RMB", InputType = SkillInputType.RightClick, Priority = 1, RangeMax = 150f, RequiresTarget = true }, - new() { SlotIndex = 3, Label = "Q", InputType = SkillInputType.KeyPress, ScanCode = 0x10, Priority = 3, RangeMax = 600f, CooldownMs = 500, MinMonstersInRange = 2 }, - new() { SlotIndex = 4, Label = "E", InputType = SkillInputType.KeyPress, ScanCode = 0x12, Priority = 4, RangeMax = 800f, CooldownMs = 300 }, + new() { SlotIndex = 0, Label = "LMB", InputType = SkillInputType.LeftClick, Priority = 0, RangeMax = 350f }, + new() { SlotIndex = 1, Label = "RMB", InputType = SkillInputType.RightClick, Priority = 1, RangeMax = 350f, CooldownMs = 800 }, + new() { SlotIndex = 3, Label = "Q", InputType = SkillInputType.KeyPress, ScanCode = 0x10, Priority = 3, RangeMax = 350f, CooldownMs = 2000, MinMonstersInRange = 3 }, + new() { SlotIndex = 4, Label = "E", InputType = SkillInputType.KeyPress, ScanCode = 0x12, Priority = 4, RangeMax = 800f, CooldownMs = 1500 }, ], Combat = new CombatSettings { GlobalCooldownMs = 400, - AttackRange = 600f, + AttackRange = 350f, }, }; @@ -88,7 +71,10 @@ nav.Explore(); // ── Bot logic thread ── var actionQueue = new ActionQueue(); +var movementBlender = new MovementBlender(); +var moveTracker = new MovementKeyTracker(); var botRunning = true; +var lastStatusLogMs = 0L; var botThread = new Thread(() => { @@ -102,26 +88,36 @@ var botThread = new Thread(() => var state = cache.LatestState; if (state is not null && !state.IsLoading && !state.IsEscapeOpen) { - // Enrich - GameStateEnricher.Enrich(state); + var resolved = BotTick.Run(state, systems, actionQueue, movementBlender, nav, botConfig); + ActionExecutor.Execute(resolved, input, moveTracker, movementBlender, state.Player.Position); - // Clear and run systems - actionQueue.Clear(); - nav.Update(state); - - foreach (var sys in systems) + // Periodic status log (every 2 seconds) + var nowMs = Environment.TickCount64; + if (nowMs - lastStatusLogMs >= 2000) { - if (sys.IsEnabled) - sys.Update(state, actionQueue); + lastStatusLogMs = nowMs; + var p = state.Player; + var enemySnapshot = world.Enemies.ToArray(); + var melee = enemySnapshot.Count(e => e.IsAlive && !e.IsRanged); + var ranged = enemySnapshot.Count(e => e.IsAlive && e.IsRanged); + var actions = string.Join(",", resolved.Select(a => a switch + { + CastAction c => $"Cast({c.SkillScanCode:X2})", + FlaskAction => "Flask", + ClickAction => "Click", + KeyAction k => $"Key({k.ScanCode:X2})", + _ => a.GetType().Name, + })); + if (actions.Length == 0) actions = "none"; + + Log.Information( + "Status: HP={HP}/{MaxHP} ES={ES}/{MaxES} Mana={MP}/{MaxMP} Danger={Danger} " + + "Enemies={Total}({Melee}m/{Ranged}r) Nav={NavMode} Actions=[{Actions}] " + + "Move=[{Blender}]", + p.LifeCurrent, p.LifeTotal, p.EsCurrent, p.EsTotal, p.ManaCurrent, p.ManaTotal, + state.Danger, melee + ranged, melee, ranged, + nav.Mode, actions, movementBlender.DiagnosticSummary()); } - - // Nav direction - if (nav.DesiredDirection.HasValue) - actionQueue.Submit(new MoveAction(SystemPriority.Navigation, nav.DesiredDirection.Value)); - - // Resolve and execute - var resolved = actionQueue.Resolve(); - ExecuteActions(resolved, state, input); } } catch (Exception ex) @@ -163,7 +159,7 @@ var imguiRenderer = new VeldridImGuiRenderer( gd, gd.MainSwapchain.Framebuffer.OutputDescription, window.Width, window.Height); -var renderer = new SimRenderer(simConfig, world, nav, systems); +var renderer = new SimRenderer(simConfig, world, nav, systems, input); var cl = gd.ResourceFactory.CreateCommandList(); window.Resized += () => @@ -187,7 +183,8 @@ while (window.Exists) imguiRenderer.Update(deltaSeconds, snapshot); - // Render sim world + // Update input flash timers & render sim world + input.UpdateTimers(deltaSeconds); renderer.Render(cache.LatestState); cl.Begin(); @@ -208,57 +205,3 @@ imguiRenderer.Dispose(); gd.Dispose(); Log.Information("Nexus Simulator stopped."); -// ── Helper: Execute bot actions via SimInputController ── -static void ExecuteActions(List resolved, GameState state, SimInputController input) -{ - foreach (var action in resolved) - { - switch (action) - { - case MoveAction move: - // MovementKeyTracker equivalent — convert direction to WASD key presses - // The SimInputController interprets these directly - const float cos45 = 0.70710678f; - const float sin45 = 0.70710678f; - var sx = move.Direction.X * cos45 - move.Direction.Y * sin45; - var sy = move.Direction.X * sin45 + move.Direction.Y * cos45; - - const float threshold = 0.3f; - if (sy > threshold) input.KeyDown(0x11); else input.KeyUp(0x11); // W - if (sy < -threshold) input.KeyDown(0x1F); else input.KeyUp(0x1F); // S - if (sx > threshold) input.KeyDown(0x20); else input.KeyUp(0x20); // D - if (sx < -threshold) input.KeyDown(0x1E); else input.KeyUp(0x1E); // A - break; - - case CastAction cast: - if (cast.TargetScreenPos.HasValue) - input.SmoothMoveTo((int)cast.TargetScreenPos.Value.X, (int)cast.TargetScreenPos.Value.Y); - input.KeyPress(cast.SkillScanCode); - break; - - case FlaskAction flask: - input.KeyPress(flask.FlaskScanCode); - break; - - case ClickAction click: - var cx = (int)click.ScreenPosition.X; - var cy = (int)click.ScreenPosition.Y; - switch (click.Type) - { - case ClickType.Left: input.LeftClick(cx, cy); break; - case ClickType.Right: input.RightClick(cx, cy); break; - case ClickType.Middle: input.MiddleClick(cx, cy); break; - } - break; - - case KeyAction key: - switch (key.Type) - { - case KeyActionType.Press: input.KeyPress(key.ScanCode); break; - case KeyActionType.Down: input.KeyDown(key.ScanCode); break; - case KeyActionType.Up: input.KeyUp(key.ScanCode); break; - } - break; - } - } -} diff --git a/src/Nexus.Simulator/Rendering/DebugPanel.cs b/src/Nexus.Simulator/Rendering/DebugPanel.cs index cbb586b..9b67f9c 100644 --- a/src/Nexus.Simulator/Rendering/DebugPanel.cs +++ b/src/Nexus.Simulator/Rendering/DebugPanel.cs @@ -64,12 +64,13 @@ public class DebugPanel // Enemy stats if (ImGui.CollapsingHeader("Enemies", ImGuiTreeNodeFlags.DefaultOpen)) { - var alive = _world.Enemies.Count(e => e.IsAlive); - var dead = _world.Enemies.Count(e => !e.IsAlive); - var chasing = _world.Enemies.Count(e => e.AiState == EnemyAiState.Chasing); - var attacking = _world.Enemies.Count(e => e.AiState == EnemyAiState.Attacking); + var enemies = _world.Enemies.ToArray(); // snapshot — list mutated by SimPoller thread + var alive = enemies.Count(e => e.IsAlive); + var dead = enemies.Count(e => !e.IsAlive); + var chasing = enemies.Count(e => e.AiState == EnemyAiState.Chasing); + var attacking = enemies.Count(e => e.AiState == EnemyAiState.Attacking); - ImGui.Text($"Total: {_world.Enemies.Count} Alive: {alive} Dead: {dead}"); + ImGui.Text($"Total: {enemies.Length} Alive: {alive} Dead: {dead}"); ImGui.Text($"Chasing: {chasing} Attacking: {attacking}"); ImGui.Separator(); diff --git a/src/Nexus.Simulator/Rendering/EffectRenderer.cs b/src/Nexus.Simulator/Rendering/EffectRenderer.cs index 45a8cb6..a402aeb 100644 --- a/src/Nexus.Simulator/Rendering/EffectRenderer.cs +++ b/src/Nexus.Simulator/Rendering/EffectRenderer.cs @@ -6,25 +6,24 @@ namespace Nexus.Simulator.Rendering; public static class EffectRenderer { - public static void DrawEffects(ImDrawListPtr drawList, List effects, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + public static void DrawEffects(ImDrawListPtr drawList, IReadOnlyList effects, ViewTransform vt) { foreach (var effect in effects) { var alpha = (byte)(255 * (1f - effect.Progress)); - var originScreen = canvasOrigin + viewOffset + effect.Origin * worldToGrid * zoom; - var targetScreen = canvasOrigin + viewOffset + effect.TargetPosition * worldToGrid * zoom; + var originScreen = vt.WorldToScreen(effect.Origin); + var targetScreen = vt.WorldToScreen(effect.TargetPosition); switch (effect.Type) { case SkillEffectType.Melee: DrawMeleeCone(drawList, originScreen, targetScreen, - effect.Radius * worldToGrid * zoom, effect.ConeAngle, alpha); + effect.Radius * vt.WorldScale, effect.ConeAngle, alpha); break; case SkillEffectType.Aoe: DrawAoeCircle(drawList, targetScreen, - effect.Radius * worldToGrid * zoom, alpha); + effect.Radius * vt.WorldScale, alpha); break; case SkillEffectType.Projectile: @@ -34,15 +33,25 @@ public static class EffectRenderer } } - public static void DrawProjectiles(ImDrawListPtr drawList, List projectiles, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + public static void DrawProjectiles(ImDrawListPtr drawList, IReadOnlyList projectiles, ViewTransform vt) { foreach (var proj in projectiles) { - var pos = canvasOrigin + viewOffset + proj.Position * worldToGrid * zoom; - var radius = proj.HitRadius * worldToGrid * zoom * 0.3f; - drawList.AddCircleFilled(pos, Math.Max(3f, radius), 0xFF00DDFF); - drawList.AddCircle(pos, Math.Max(4f, radius + 1), 0xFF00AAFF); + var pos = vt.WorldToScreen(proj.Position); + var radius = proj.HitRadius * vt.WorldScale * 0.3f; + + if (proj.IsEnemyProjectile) + { + // Red/orange for enemy projectiles + drawList.AddCircleFilled(pos, Math.Max(3f, radius), 0xFF3344FF); + drawList.AddCircle(pos, Math.Max(4f, radius + 1), 0xFF0000FF); + } + else + { + // Cyan for player projectiles + drawList.AddCircleFilled(pos, Math.Max(3f, radius), 0xFF00DDFF); + drawList.AddCircle(pos, Math.Max(4f, radius + 1), 0xFF00AAFF); + } } } diff --git a/src/Nexus.Simulator/Rendering/EntityRenderer.cs b/src/Nexus.Simulator/Rendering/EntityRenderer.cs index 52aaac1..b099953 100644 --- a/src/Nexus.Simulator/Rendering/EntityRenderer.cs +++ b/src/Nexus.Simulator/Rendering/EntityRenderer.cs @@ -7,33 +7,40 @@ namespace Nexus.Simulator.Rendering; public static class EntityRenderer { - public static void DrawPlayer(ImDrawListPtr drawList, SimPlayer player, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + public static void DrawPlayer(ImDrawListPtr drawList, SimPlayer player, ViewTransform vt) { - var gridPos = player.Position * worldToGrid; - var screenPos = canvasOrigin + viewOffset + gridPos * zoom; + var screenPos = vt.WorldToScreen(player.Position); var radius = 8f; drawList.AddCircleFilled(screenPos, radius, 0xFF00FF00); // Green drawList.AddCircle(screenPos, radius + 1, 0xFF00AA00); - // Health bar above player - DrawHealthBar(drawList, screenPos - new Vector2(15, radius + 8), 30, 4, + var barY = radius + 8; + + // ES bar (purple, above health) + if (player.MaxEs > 0) + { + DrawHealthBar(drawList, screenPos - new Vector2(15, barY), 30, 3, + player.Es, player.MaxEs, 0xFFFF8800); // Cyan/purple in ABGR + barY += 5; + } + + // Health bar + DrawHealthBar(drawList, screenPos - new Vector2(15, barY), 30, 4, player.Health, player.MaxHealth, 0xFF00DD00); + barY += 6; // Mana bar - DrawHealthBar(drawList, screenPos - new Vector2(15, radius + 14), 30, 3, + DrawHealthBar(drawList, screenPos - new Vector2(15, barY), 30, 3, player.Mana, player.MaxMana, 0xFFDD6600); } - public static void DrawEnemies(ImDrawListPtr drawList, List enemies, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid, - Vector2 canvasMin, Vector2 canvasMax) + public static void DrawEnemies(ImDrawListPtr drawList, IReadOnlyList enemies, + ViewTransform vt, Vector2 canvasMin, Vector2 canvasMax) { foreach (var enemy in enemies) { - var gridPos = enemy.Position * worldToGrid; - var screenPos = canvasOrigin + viewOffset + gridPos * zoom; + var screenPos = vt.WorldToScreen(enemy.Position); // Cull off-screen if (screenPos.X < canvasMin.X - 20 || screenPos.X > canvasMax.X + 20 || @@ -54,12 +61,30 @@ public static class EntityRenderer radius *= 0.7f; } - drawList.AddCircleFilled(screenPos, radius, color); + if (enemy.IsRanged) + { + // Diamond shape for ranged enemies + var s = radius * 1.2f; + var pts = new[] + { + screenPos + new Vector2(0, -s), + screenPos + new Vector2(s, 0), + screenPos + new Vector2(0, s), + screenPos + new Vector2(-s, 0), + }; + drawList.AddQuadFilled(pts[0], pts[1], pts[2], pts[3], color); + } + else + { + drawList.AddCircleFilled(screenPos, radius, color); + } if (enemy.AiState == EnemyAiState.Chasing) drawList.AddCircle(screenPos, radius + 2, 0xFF0000FF); // Red ring when chasing else if (enemy.AiState == EnemyAiState.Attacking) drawList.AddCircle(screenPos, radius + 3, 0xFF0000FF, 0, 3f); + else if (enemy.AiState == EnemyAiState.Retreating) + drawList.AddCircle(screenPos, radius + 2, 0xFF00AAAA); // Teal ring when retreating // Health bar if (enemy.IsAlive && enemy.Health < enemy.MaxHealth) diff --git a/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs b/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs new file mode 100644 index 0000000..6695c88 --- /dev/null +++ b/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs @@ -0,0 +1,155 @@ +using System.Numerics; +using ImGuiNET; +using Nexus.Simulator.Bridge; + +namespace Nexus.Simulator.Rendering; + +/// +/// Draws a keyboard + mouse + mousepad overlay showing which keys/buttons are currently pressed. +/// Keys are white normally, yellow when active. +/// +public static class InputOverlayRenderer +{ + private const float KeySize = 26f; + private const float Gap = 2f; + private const float Stride = KeySize + Gap; + + // Colors (ABGR) + private const uint White = 0xFFFFFFFF; + private const uint Yellow = 0xFF00FFFF; + private const uint DarkBg = 0xFF1E1E1E; + private const uint ActiveBg = 0xFF1A5C8C; + private const uint Outline = 0xFF555555; + private const uint DimText = 0xFFAAAAAA; + private const uint ScrollBg = 0xFF333333; + private const uint CursorDot = 0xFF00DDFF; // Cyan dot for cursor position + private const uint CrosshairColor = 0x44FFFFFF; // Dim crosshair + + // Keyboard rows: (label, scanCode, column offset) + private static readonly (string L, ushort S, float C)[] Row0 = + [("1", 0x02, 0), ("2", 0x03, 1), ("3", 0x04, 2), ("4", 0x05, 3), ("5", 0x06, 4)]; + + private static readonly (string L, ushort S, float C)[] Row1 = + [("Q", 0x10, 0.25f), ("W", 0x11, 1.25f), ("E", 0x12, 2.25f), ("R", 0x13, 3.25f), ("T", 0x14, 4.25f)]; + + private static readonly (string L, ushort S, float C)[] Row2 = + [("A", 0x1E, 0.5f), ("S", 0x1F, 1.5f), ("D", 0x20, 2.5f), ("F", 0x21, 3.5f)]; + + // Screen dimensions the bot thinks it has + private const float ScreenW = 2560f; + private const float ScreenH = 1440f; + + public static void Draw(ImDrawListPtr drawList, InputSnapshot input, Vector2 mouseScreenPos, + Vector2 canvasOrigin, Vector2 canvasSize) + { + var padSize = 80f; + var mouseH = 64f; + var kbH = 3 * Stride; + var totalH = kbH + 6 + mouseH + 6 + padSize; + var origin = canvasOrigin + new Vector2(15, canvasSize.Y - totalH - 15); + + // Keyboard + DrawKeyRow(drawList, origin, Row0, 0, input); + DrawKeyRow(drawList, origin, Row1, 1, input); + DrawKeyRow(drawList, origin, Row2, 2, input); + + // Mouse to the right of keyboard + var kbW = 4.25f * Stride + KeySize; + var mouseOrigin = origin + new Vector2(kbW + 12, (kbH - mouseH) / 2); + DrawMouse(drawList, input, mouseOrigin); + + // Mousepad below keyboard + mouse row + var padOrigin = origin + new Vector2(0, kbH + 6); + DrawMousepad(drawList, mouseScreenPos, input, padOrigin, kbW + 12 + 44); + } + + private static void DrawKeyRow(ImDrawListPtr drawList, Vector2 origin, + (string L, ushort S, float C)[] keys, int row, InputSnapshot input) + { + foreach (var (label, scan, col) in keys) + { + var pos = origin + new Vector2(col * Stride, row * Stride); + var on = input.IsKeyActive(scan); + + drawList.AddRectFilled(pos, pos + new Vector2(KeySize), on ? ActiveBg : DarkBg, 3f); + drawList.AddRect(pos, pos + new Vector2(KeySize), on ? Yellow : Outline, 3f); + + var ts = ImGui.CalcTextSize(label); + drawList.AddText(pos + (new Vector2(KeySize) - ts) * 0.5f, on ? Yellow : White, label); + } + } + + private static void DrawMouse(ImDrawListPtr drawList, InputSnapshot input, Vector2 o) + { + const float w = 44, h = 64, hw = w / 2, bh = 26; + + // Body + drawList.AddRectFilled(o, o + new Vector2(w, h), DarkBg, 10f); + drawList.AddRect(o, o + new Vector2(w, h), Outline, 10f); + + // Left button highlight + var lOn = input.IsMouseActive(0); + if (lOn) + drawList.AddRectFilled(o + new Vector2(2, 2), o + new Vector2(hw - 1, bh), ActiveBg, 4f); + + // Right button highlight + var rOn = input.IsMouseActive(1); + if (rOn) + drawList.AddRectFilled(o + new Vector2(hw + 1, 2), o + new Vector2(w - 2, bh), ActiveBg, 4f); + + // Dividers + drawList.AddLine(o + new Vector2(hw, 2), o + new Vector2(hw, bh), Outline); + drawList.AddLine(o + new Vector2(4, bh), o + new Vector2(w - 4, bh), Outline); + + // Scroll wheel + var mOn = input.IsMouseActive(2); + var sw = new Vector2(8, 14); + var sp = o + new Vector2((w - sw.X) / 2, (bh - sw.Y) / 2); + drawList.AddRectFilled(sp, sp + sw, mOn ? ActiveBg : ScrollBg, 3f); + drawList.AddRect(sp, sp + sw, mOn ? Yellow : Outline, 3f); + + // Labels + DrawCentered(drawList, "L", o + new Vector2(hw / 2, bh / 2), lOn ? Yellow : DimText); + DrawCentered(drawList, "R", o + new Vector2(hw + hw / 2, bh / 2), rOn ? Yellow : DimText); + } + + private static void DrawMousepad(ImDrawListPtr drawList, Vector2 mouseScreenPos, + InputSnapshot input, Vector2 origin, float width) + { + // Mousepad: maps the bot's 2560x1440 screen space to a small rectangle + var aspect = ScreenH / ScreenW; + var padW = width; + var padH = padW * aspect; + + // Background + drawList.AddRectFilled(origin, origin + new Vector2(padW, padH), DarkBg, 4f); + drawList.AddRect(origin, origin + new Vector2(padW, padH), Outline, 4f); + + // Crosshair at center + var center = origin + new Vector2(padW, padH) * 0.5f; + drawList.AddLine(center - new Vector2(8, 0), center + new Vector2(8, 0), CrosshairColor); + drawList.AddLine(center - new Vector2(0, 8), center + new Vector2(0, 8), CrosshairColor); + + // Map mouse screen position to pad coordinates + var nx = Math.Clamp(mouseScreenPos.X / ScreenW, 0f, 1f); + var ny = Math.Clamp(mouseScreenPos.Y / ScreenH, 0f, 1f); + var dotPos = origin + new Vector2(nx * padW, ny * padH); + + // Cursor dot — changes color on click + var anyClick = input.IsMouseActive(0) || input.IsMouseActive(1) || input.IsMouseActive(2); + var dotColor = anyClick ? Yellow : CursorDot; + drawList.AddCircleFilled(dotPos, 4f, dotColor); + drawList.AddCircle(dotPos, 5f, anyClick ? Yellow : Outline); + + // Coordinates text + var coordText = $"{mouseScreenPos.X:F0},{mouseScreenPos.Y:F0}"; + var ts = ImGui.CalcTextSize(coordText); + drawList.AddText(origin + new Vector2(padW - ts.X - 4, padH - ts.Y - 2), DimText, coordText); + } + + private static void DrawCentered(ImDrawListPtr drawList, string text, Vector2 center, uint color) + { + var ts = ImGui.CalcTextSize(text); + drawList.AddText(center - ts * 0.5f, color, text); + } +} diff --git a/src/Nexus.Simulator/Rendering/PathRenderer.cs b/src/Nexus.Simulator/Rendering/PathRenderer.cs index 66335b7..314309b 100644 --- a/src/Nexus.Simulator/Rendering/PathRenderer.cs +++ b/src/Nexus.Simulator/Rendering/PathRenderer.cs @@ -6,32 +6,23 @@ namespace Nexus.Simulator.Rendering; public static class PathRenderer { - public static void Draw(ImDrawListPtr drawList, NavigationController nav, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, float worldToGrid) + public static void Draw(ImDrawListPtr drawList, NavigationController nav, ViewTransform vt) { var path = nav.CurrentPath; if (path is null || path.Count < 2) return; for (var i = 0; i < path.Count - 1; i++) { - var a = canvasOrigin + viewOffset + path[i] * worldToGrid * zoom; - var b = canvasOrigin + viewOffset + path[i + 1] * worldToGrid * zoom; + var a = vt.WorldToScreen(path[i]); + var b = vt.WorldToScreen(path[i + 1]); drawList.AddLine(a, b, 0xFFFFFF00, 2f); // Cyan } // Draw waypoint dots foreach (var wp in path) { - var pos = canvasOrigin + viewOffset + wp * worldToGrid * zoom; + var pos = vt.WorldToScreen(wp); drawList.AddCircleFilled(pos, 3f, 0xFFFFFF00); } } - - public static void DrawExploredOverlay(ImDrawListPtr drawList, - bool[]? exploredGrid, int exploredWidth, int exploredHeight, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, Vector2 canvasSize) - { - // Already handled in TerrainRenderer via brightness difference - // This method is a placeholder for additional explore visualization - } } diff --git a/src/Nexus.Simulator/Rendering/SimRenderer.cs b/src/Nexus.Simulator/Rendering/SimRenderer.cs index c8bae7c..79d3860 100644 --- a/src/Nexus.Simulator/Rendering/SimRenderer.cs +++ b/src/Nexus.Simulator/Rendering/SimRenderer.cs @@ -2,31 +2,35 @@ using System.Numerics; using ImGuiNET; using Nexus.Core; using Nexus.Pathfinding; +using Nexus.Simulator.Bridge; using Nexus.Simulator.Config; using Nexus.Simulator.World; namespace Nexus.Simulator.Rendering; /// -/// Main renderer: draws the top-down game world viewport using ImGui draw lists. +/// Main renderer: draws the 45°-rotated isometric game world viewport using ImGui draw lists. /// public class SimRenderer { private readonly SimConfig _config; private readonly SimWorld _world; private readonly NavigationController _nav; + private readonly SimInputController _input; private readonly DebugPanel _debugPanel; // Camera private Vector2 _viewOffset; private float _zoom = 2f; // pixels per grid cell + private const float C = 0.70710678f; public SimRenderer(SimConfig config, SimWorld world, NavigationController nav, - IReadOnlyList systems) + IReadOnlyList systems, SimInputController input) { _config = config; _world = world; _nav = nav; + _input = input; _debugPanel = new DebugPanel(config, world, nav, systems); // Center view on player @@ -53,31 +57,39 @@ public class SimRenderer CenterOnPlayer(canvasSize); var drawList = ImGui.GetWindowDrawList(); + var vt = new ViewTransform(canvasOrigin, _viewOffset, _zoom, _config.WorldToGrid); // Clip to canvas drawList.PushClipRect(canvasOrigin, canvasOrigin + canvasSize); // 1. Terrain - TerrainRenderer.Draw(drawList, _world.Terrain, _viewOffset, _zoom, canvasOrigin, canvasSize, - _nav.ExploredGrid, _nav.ExploredWidth, _nav.ExploredHeight); + TerrainRenderer.Draw(drawList, _world.Terrain, vt, canvasSize, + _nav.ExploredGrid, _nav.ExploredWidth, _nav.ExploredHeight, + _nav.ExploredOffsetX, _nav.ExploredOffsetY); // 2. Path - PathRenderer.Draw(drawList, _nav, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + PathRenderer.Draw(drawList, _nav, vt); - // 3. Effects - EffectRenderer.DrawEffects(drawList, _world.ActiveEffects, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); - EffectRenderer.DrawProjectiles(drawList, _world.Projectiles, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + // 3. Effects — snapshot shared lists to avoid concurrent modification from SimPoller thread + var effects = _world.ActiveEffects.ToArray(); + var projectiles = _world.Projectiles.ToArray(); + var enemies = _world.Enemies.ToArray(); + + EffectRenderer.DrawEffects(drawList, effects, vt); + EffectRenderer.DrawProjectiles(drawList, projectiles, vt); // 4. Enemies - EntityRenderer.DrawEnemies(drawList, _world.Enemies, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid, - canvasOrigin, canvasOrigin + canvasSize); + EntityRenderer.DrawEnemies(drawList, enemies, vt, canvasOrigin, canvasOrigin + canvasSize); // 5. Player - EntityRenderer.DrawPlayer(drawList, _world.Player, _viewOffset, _zoom, canvasOrigin, _config.WorldToGrid); + EntityRenderer.DrawPlayer(drawList, _world.Player, vt); + + // 6. Mock cursor — shows where the bot's mouse is pointing in the world + DrawMockCursor(drawList, vt); drawList.PopClipRect(); - // Minimap (bottom-right corner) + // Minimap (bottom-right corner, top-down view) var minimapSize = 150f; var minimapOrigin = canvasOrigin + canvasSize - new Vector2(minimapSize + 10, minimapSize + 10); var playerGridPos = _world.Player.Position * _config.WorldToGrid; @@ -86,14 +98,22 @@ public class SimRenderer // HUD text DrawHud(drawList, canvasOrigin, state); + // Input overlay (keyboard + mouse + mousepad) + InputOverlayRenderer.Draw(drawList, _input.GetInputSnapshot(), _input.MouseScreenPos, + canvasOrigin, canvasSize); + ImGui.End(); } private void CenterOnPlayer(Vector2? canvasSize = null) { var cs = canvasSize ?? new Vector2(1200, 900); - var playerGrid = _world.Player.Position * _config.WorldToGrid; - _viewOffset = cs * 0.5f - playerGrid * _zoom; + var gx = _world.Player.Position.X * _config.WorldToGrid; + var gy = _world.Player.Position.Y * _config.WorldToGrid; + // Rotated grid position + var rx = (gx - gy) * C; + var ry = -(gx + gy) * C; + _viewOffset = cs * 0.5f - new Vector2(rx, ry) * _zoom; } private void HandleInput(Vector2 canvasOrigin, Vector2 canvasSize) @@ -102,7 +122,7 @@ public class SimRenderer var io = ImGui.GetIO(); - // Scroll to zoom + // Scroll to zoom (works in rotated space — no change needed) if (io.MouseWheel != 0) { var mousePos = io.MousePos - canvasOrigin; @@ -115,6 +135,37 @@ public class SimRenderer } } + private void DrawMockCursor(ImDrawListPtr drawList, ViewTransform vt) + { + // Convert the bot's mouse screen position to world, then to our viewport + var mouseWorld = _world.MouseWorldPos; + var screenPos = vt.WorldToScreen(mouseWorld); + + const float s = 12f; // cursor size + var col = 0xFFFFFFFF; // white + var shadow = 0xAA000000; // black shadow + + // Arrow cursor shape (triangle + tail) + var tip = screenPos; + var left = tip + new Vector2(0, s); + var right = tip + new Vector2(s * 0.55f, s * 0.75f); + var mid = tip + new Vector2(s * 0.2f, s * 0.65f); + var tailEnd = tip + new Vector2(s * 0.55f, s * 1.1f); + var tailRight = tip + new Vector2(s * 0.4f, s * 0.95f); + + // Shadow (offset by 1px) + var off = new Vector2(1, 1); + drawList.AddTriangleFilled(tip + off, left + off, right + off, shadow); + drawList.AddTriangleFilled(mid + off, tailEnd + off, tailRight + off, shadow); + + // Cursor body + drawList.AddTriangleFilled(tip, left, right, col); + drawList.AddTriangleFilled(mid, tailEnd, tailRight, col); + + // Outline + drawList.AddTriangle(tip, left, right, 0xFF000000); + } + private void DrawHud(ImDrawListPtr drawList, Vector2 canvasOrigin, GameState? state) { var textPos = canvasOrigin + new Vector2(10, 10); diff --git a/src/Nexus.Simulator/Rendering/TerrainRenderer.cs b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs index 6722a74..b68d76c 100644 --- a/src/Nexus.Simulator/Rendering/TerrainRenderer.cs +++ b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs @@ -7,23 +7,38 @@ namespace Nexus.Simulator.Rendering; public static class TerrainRenderer { /// - /// Draws the walkability grid as colored rectangles on the ImGui draw list. + /// Draws the walkability grid as rotated diamond cells on the ImGui draw list. /// Only draws cells visible in the current viewport for performance. /// public static void Draw(ImDrawListPtr drawList, WalkabilitySnapshot terrain, - Vector2 viewOffset, float zoom, Vector2 canvasOrigin, Vector2 canvasSize, - bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0) + ViewTransform vt, Vector2 canvasSize, + bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0, + int exploredOffsetX = 0, int exploredOffsetY = 0) { - var cellSize = zoom; - if (cellSize < 0.5f) return; // Too zoomed out to draw individual cells + var cellSize = vt.Zoom; + if (cellSize < 0.5f) return; - // Visible range in grid coords - var minGx = Math.Max(0, (int)(-viewOffset.X / cellSize)); - var minGy = Math.Max(0, (int)(-viewOffset.Y / cellSize)); - var maxGx = Math.Min(terrain.Width - 1, (int)((-viewOffset.X + canvasSize.X) / cellSize)); - var maxGy = Math.Min(terrain.Height - 1, (int)((-viewOffset.Y + canvasSize.Y) / cellSize)); + var ox = terrain.OffsetX; + var oy = terrain.OffsetY; - // Skip pixels if too many cells + // Compute visible grid bounds from screen corners (inverse transform) + var c0 = vt.ScreenToGrid(0, 0); + var c1 = vt.ScreenToGrid(canvasSize.X, 0); + var c2 = vt.ScreenToGrid(canvasSize.X, canvasSize.Y); + var c3 = vt.ScreenToGrid(0, canvasSize.Y); + + var minGx = (int)MathF.Floor(Min4(c0.X, c1.X, c2.X, c3.X)); + var maxGx = (int)MathF.Ceiling(Max4(c0.X, c1.X, c2.X, c3.X)); + var minGy = (int)MathF.Floor(Min4(c0.Y, c1.Y, c2.Y, c3.Y)); + var maxGy = (int)MathF.Ceiling(Max4(c0.Y, c1.Y, c2.Y, c3.Y)); + + // Clamp to terrain bounds + minGx = Math.Max(ox, minGx); + maxGx = Math.Min(ox + terrain.Width - 1, maxGx); + minGy = Math.Max(oy, minGy); + maxGy = Math.Min(oy + terrain.Height - 1, maxGy); + + // Skip cells if too many var step = 1; if (cellSize < 2f) step = 4; else if (cellSize < 4f) step = 2; @@ -31,9 +46,6 @@ public static class TerrainRenderer for (var gy = minGy; gy <= maxGy; gy += step) for (var gx = minGx; gx <= maxGx; gx += step) { - var screenX = canvasOrigin.X + viewOffset.X + gx * cellSize; - var screenY = canvasOrigin.Y + viewOffset.Y + gy * cellSize; - var w = terrain.IsWalkable(gx, gy); uint color; @@ -43,23 +55,26 @@ public static class TerrainRenderer } else { + var elx = gx - exploredOffsetX; + var ely = gy - exploredOffsetY; var explored = exploredGrid is not null - && gx < exploredWidth && gy < exploredHeight - && exploredGrid[gy * exploredWidth + gx]; + && elx >= 0 && elx < exploredWidth && ely >= 0 && ely < exploredHeight + && exploredGrid[ely * exploredWidth + elx]; - color = explored ? 0xFF3D3D5C : 0xFF2A2A3F; // Brighter if explored + color = explored ? 0xFF3D3D5C : 0xFF2A2A3F; } - var size = cellSize * step; - drawList.AddRectFilled( - new Vector2(screenX, screenY), - new Vector2(screenX + size, screenY + size), - color); + // Draw diamond (rotated grid cell) + var p0 = vt.GridToScreen(gx, gy); // top + var p1 = vt.GridToScreen(gx + step, gy); // right + var p2 = vt.GridToScreen(gx + step, gy + step); // bottom + var p3 = vt.GridToScreen(gx, gy + step); // left + drawList.AddQuadFilled(p0, p1, p2, p3, color); } } /// - /// Draws a minimap in the corner. + /// Draws a minimap in the corner (top-down, no rotation). /// public static void DrawMinimap(ImDrawListPtr drawList, WalkabilitySnapshot terrain, Vector2 playerGridPos, Vector2 minimapOrigin, float minimapSize) @@ -73,22 +88,23 @@ public static class TerrainRenderer minimapOrigin + new Vector2(terrain.Width * scale, terrain.Height * scale), 0xFF0A0A15); - // Draw walkable cells (sampled) + // Draw walkable cells (sampled) — local coords var step = Math.Max(1, terrain.Width / 200); - for (var gy = 0; gy < terrain.Height; gy += step) - for (var gx = 0; gx < terrain.Width; gx += step) + for (var ly = 0; ly < terrain.Height; ly += step) + for (var lx = 0; lx < terrain.Width; lx += step) { - if (!terrain.IsWalkable(gx, gy)) continue; - var px = minimapOrigin.X + gx * scale; - var py = minimapOrigin.Y + gy * scale; + if (!terrain.IsWalkable(lx + terrain.OffsetX, ly + terrain.OffsetY)) continue; + var px = minimapOrigin.X + lx * scale; + var py = minimapOrigin.Y + ly * scale; drawList.AddRectFilled( new Vector2(px, py), new Vector2(px + scale * step, py + scale * step), 0xFF2A2A3F); } - // Player dot - var playerPx = minimapOrigin + playerGridPos * scale; + // Player dot — convert absolute grid pos to local + var playerLocalPos = playerGridPos - new Vector2(terrain.OffsetX, terrain.OffsetY); + var playerPx = minimapOrigin + playerLocalPos * scale; drawList.AddCircleFilled(playerPx, 3f, 0xFF00FF00); // Border @@ -96,4 +112,10 @@ public static class TerrainRenderer minimapOrigin + new Vector2(terrain.Width * scale, terrain.Height * scale), 0xFF666666); } + + private static float Min4(float a, float b, float c, float d) + => MathF.Min(MathF.Min(a, b), MathF.Min(c, d)); + + private static float Max4(float a, float b, float c, float d) + => MathF.Max(MathF.Max(a, b), MathF.Max(c, d)); } diff --git a/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs b/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs index 85eaf71..28d01cc 100644 --- a/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs +++ b/src/Nexus.Simulator/Rendering/VeldridImGuiRenderer.cs @@ -21,10 +21,11 @@ public sealed class VeldridImGuiRenderer : IDisposable private Texture _fontTexture = null!; private TextureView _fontTextureView = null!; private ResourceSet _fontResourceSet = null!; - private ResourceLayout _layout = null!; + private ResourceLayout _projSamplerLayout = null!; private ResourceLayout _textureLayout = null!; private Pipeline _pipeline = null!; private Shader[] _shaders = null!; + private ResourceSet _projSamplerSet = null!; private int _windowWidth; private int _windowHeight; @@ -49,7 +50,6 @@ public sealed class VeldridImGuiRenderer : IDisposable io.DisplayFramebufferScale = Vector2.One; CreateDeviceResources(gd, outputDescription); - SetupKeyMappings(); } public void WindowResized(int width, int height) @@ -84,14 +84,11 @@ public sealed class VeldridImGuiRenderer : IDisposable _indexBuffer = factory.CreateBuffer(new BufferDescription(2000, BufferUsage.IndexBuffer | BufferUsage.Dynamic)); _projMatrixBuffer = factory.CreateBuffer(new BufferDescription(64, BufferUsage.UniformBuffer | BufferUsage.Dynamic)); - // Create shaders using HLSL + // Create shaders _shaders = CreateShaders(gd); - // Font texture - RecreateFontDeviceTexture(gd); - // Resource layouts - _layout = factory.CreateResourceLayout(new ResourceLayoutDescription( + _projSamplerLayout = factory.CreateResourceLayout(new ResourceLayoutDescription( new ResourceLayoutElementDescription("ProjectionMatrixBuffer", ResourceKind.UniformBuffer, ShaderStages.Vertex), new ResourceLayoutElementDescription("MainSampler", ResourceKind.Sampler, ShaderStages.Fragment))); @@ -126,12 +123,19 @@ public sealed class VeldridImGuiRenderer : IDisposable FaceCullMode.None, PolygonFillMode.Solid, FrontFace.Clockwise, true, true), PrimitiveTopology = PrimitiveTopology.TriangleList, - ResourceLayouts = [_layout, _textureLayout], + ResourceLayouts = [_projSamplerLayout, _textureLayout], ShaderSet = new ShaderSetDescription([vertexLayout], _shaders), Outputs = outputDescription, }; _pipeline = factory.CreateGraphicsPipeline(ref pipelineDesc); + + // Cached resource set for projection + sampler + _projSamplerSet = factory.CreateResourceSet(new ResourceSetDescription( + _projSamplerLayout, _projMatrixBuffer, gd.PointSampler)); + + // Font texture (MUST be after _textureLayout is created) + RecreateFontDeviceTexture(gd); } private void RecreateFontDeviceTexture(GraphicsDevice gd) @@ -141,6 +145,7 @@ public sealed class VeldridImGuiRenderer : IDisposable _fontTexture?.Dispose(); _fontTextureView?.Dispose(); + _fontResourceSet?.Dispose(); _fontTexture = gd.ResourceFactory.CreateTexture(TextureDescription.Texture2D( (uint)width, (uint)height, 1, 1, @@ -155,34 +160,18 @@ public sealed class VeldridImGuiRenderer : IDisposable io.Fonts.SetTexID(_fontAtlasId); io.Fonts.ClearTexData(); - // Create resource set for font - if (_textureLayout is not null) - { - _fontResourceSet?.Dispose(); - _fontResourceSet = gd.ResourceFactory.CreateResourceSet(new ResourceSetDescription( - _textureLayout, _fontTextureView)); - _resourceSets[_fontAtlasId] = _fontResourceSet; - } + _fontResourceSet = gd.ResourceFactory.CreateResourceSet(new ResourceSetDescription( + _textureLayout, _fontTextureView)); + _resourceSets[_fontAtlasId] = _fontResourceSet; } private Shader[] CreateShaders(GraphicsDevice gd) { - // For D3D11, use HLSL compiled at runtime - // For other backends, we'd need SPIRV - var backend = gd.BackendType; + if (gd.BackendType != GraphicsBackend.Direct3D11) + throw new NotSupportedException($"Backend {gd.BackendType} not supported. Use Direct3D11."); - byte[] vertexShaderBytes; - byte[] fragmentShaderBytes; - - if (backend == GraphicsBackend.Direct3D11) - { - vertexShaderBytes = CompileHlsl(VertexShaderHlsl, "main", "vs_5_0"); - fragmentShaderBytes = CompileHlsl(FragmentShaderHlsl, "main", "ps_5_0"); - } - else - { - throw new NotSupportedException($"Backend {backend} not supported. Use Direct3D11."); - } + var vertexShaderBytes = CompileHlsl(VertexShaderHlsl, "main", "vs_5_0"); + var fragmentShaderBytes = CompileHlsl(FragmentShaderHlsl, "main", "ps_5_0"); var vertexShader = gd.ResourceFactory.CreateShader(new ShaderDescription( ShaderStages.Vertex, vertexShaderBytes, "main")); @@ -200,6 +189,8 @@ public sealed class VeldridImGuiRenderer : IDisposable var totalVtxSize = (uint)(drawData.TotalVtxCount * Unsafe.SizeOf()); var totalIdxSize = (uint)(drawData.TotalIdxCount * sizeof(ushort)); + if (totalVtxSize == 0 || totalIdxSize == 0) return; + if (totalVtxSize > _vertexBuffer.SizeInBytes) { _vertexBuffer.Dispose(); @@ -219,16 +210,15 @@ public sealed class VeldridImGuiRenderer : IDisposable for (var i = 0; i < drawData.CmdListsCount; i++) { var cmdList = drawData.CmdLists[i]; - cl.UpdateBuffer(_vertexBuffer, vtxOffset, - cmdList.VtxBuffer.Data, (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf())); - cl.UpdateBuffer(_indexBuffer, idxOffset, - cmdList.IdxBuffer.Data, (uint)(cmdList.IdxBuffer.Size * sizeof(ushort))); - vtxOffset += (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf()); - idxOffset += (uint)(cmdList.IdxBuffer.Size * sizeof(ushort)); + var vtxSize = (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf()); + var idxSize = (uint)(cmdList.IdxBuffer.Size * sizeof(ushort)); + cl.UpdateBuffer(_vertexBuffer, vtxOffset, cmdList.VtxBuffer.Data, vtxSize); + cl.UpdateBuffer(_indexBuffer, idxOffset, cmdList.IdxBuffer.Data, idxSize); + vtxOffset += vtxSize; + idxOffset += idxSize; } // Update projection matrix - var io = ImGui.GetIO(); var mvp = Matrix4x4.CreateOrthographicOffCenter( drawData.DisplayPos.X, drawData.DisplayPos.X + drawData.DisplaySize.X, @@ -240,14 +230,11 @@ public sealed class VeldridImGuiRenderer : IDisposable cl.SetVertexBuffer(0, _vertexBuffer); cl.SetIndexBuffer(_indexBuffer, IndexFormat.UInt16); cl.SetPipeline(_pipeline); - - // Create main resource set - var mainResourceSet = gd.ResourceFactory.CreateResourceSet(new ResourceSetDescription( - _layout, _projMatrixBuffer, gd.PointSampler)); - cl.SetGraphicsResourceSet(0, mainResourceSet); + cl.SetGraphicsResourceSet(0, _projSamplerSet); // Draw var clipOff = drawData.DisplayPos; + var clipScale = drawData.FramebufferScale; vtxOffset = 0; idxOffset = 0; for (var n = 0; n < drawData.CmdListsCount; n++) @@ -263,23 +250,23 @@ public sealed class VeldridImGuiRenderer : IDisposable cl.SetGraphicsResourceSet(1, rs); } - var clipRect = pcmd.ClipRect; - cl.SetScissorRect(0, - (uint)(clipRect.X - clipOff.X), - (uint)(clipRect.Y - clipOff.Y), - (uint)(clipRect.Z - clipRect.X), - (uint)(clipRect.W - clipRect.Y)); + var clipX = (uint)Math.Max(0, (pcmd.ClipRect.X - clipOff.X) * clipScale.X); + var clipY = (uint)Math.Max(0, (pcmd.ClipRect.Y - clipOff.Y) * clipScale.Y); + var clipW = (uint)Math.Max(0, (pcmd.ClipRect.Z - pcmd.ClipRect.X) * clipScale.X); + var clipH = (uint)Math.Max(0, (pcmd.ClipRect.W - pcmd.ClipRect.Y) * clipScale.Y); + + if (clipW == 0 || clipH == 0) continue; + + cl.SetScissorRect(0, clipX, clipY, clipW, clipH); cl.DrawIndexed(pcmd.ElemCount, 1, pcmd.IdxOffset + idxOffset, - (int)(pcmd.VtxOffset + vtxOffset), + (int)(pcmd.VtxOffset + vtxOffset / (uint)Unsafe.SizeOf()), 0); } - vtxOffset += (uint)cmdList.VtxBuffer.Size; + vtxOffset += (uint)(cmdList.VtxBuffer.Size * Unsafe.SizeOf()); idxOffset += (uint)cmdList.IdxBuffer.Size; } - - mainResourceSet.Dispose(); } private void UpdateInput(InputSnapshot snapshot) @@ -327,11 +314,6 @@ public sealed class VeldridImGuiRenderer : IDisposable _ => ImGuiKey.None, }; - private void SetupKeyMappings() - { - // ImGui.NET 1.91+ uses the key event API directly, no need for explicit mappings - } - // ── HLSL Shader Sources ── private const string VertexShaderHlsl = @" @@ -387,7 +369,7 @@ float4 main(PS_INPUT input) : SV_Target [DllImport("d3dcompiler_47.dll", CallingConvention = CallingConvention.StdCall)] private static extern int D3DCompile( [MarshalAs(UnmanagedType.LPStr)] string pSrcData, - int srcDataSize, + nint srcDataSize, [MarshalAs(UnmanagedType.LPStr)] string? pSourceName, IntPtr pDefines, IntPtr pInclude, @@ -398,46 +380,40 @@ float4 main(PS_INPUT input) : SV_Target out IntPtr ppCode, out IntPtr ppErrorMsgs); - [DllImport("d3dcompiler_47.dll", CallingConvention = CallingConvention.StdCall)] - private static extern IntPtr D3DGetBlobPart(IntPtr pSrcData, int srcDataSize, int part, uint flags, out IntPtr ppPart); - // ID3DBlob vtable offsets private static IntPtr BlobGetBufferPointer(IntPtr blob) { var vtable = Marshal.ReadIntPtr(blob); - var getBufferPtr = Marshal.ReadIntPtr(vtable, 3 * IntPtr.Size); // IUnknown (3 methods) + GetBufferPointer - var del = Marshal.GetDelegateForFunctionPointer(getBufferPtr); - return del(blob); + var fn = Marshal.ReadIntPtr(vtable, 3 * IntPtr.Size); + return Marshal.GetDelegateForFunctionPointer(fn)(blob); } - private static int BlobGetBufferSize(IntPtr blob) + private static nint BlobGetBufferSize(IntPtr blob) { var vtable = Marshal.ReadIntPtr(blob); - var getBufferSize = Marshal.ReadIntPtr(vtable, 4 * IntPtr.Size); // IUnknown (3 methods) + GetBufferPointer + GetBufferSize - var del = Marshal.GetDelegateForFunctionPointer(getBufferSize); - return del(blob); + var fn = Marshal.ReadIntPtr(vtable, 4 * IntPtr.Size); + return Marshal.GetDelegateForFunctionPointer(fn)(blob); } private static void BlobRelease(IntPtr blob) { var vtable = Marshal.ReadIntPtr(blob); - var release = Marshal.ReadIntPtr(vtable, 2 * IntPtr.Size); // IUnknown::Release - var del = Marshal.GetDelegateForFunctionPointer(release); - del(blob); + var fn = Marshal.ReadIntPtr(vtable, 2 * IntPtr.Size); + Marshal.GetDelegateForFunctionPointer(fn)(blob); } [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr GetBufferPointerDelegate(IntPtr self); + private delegate IntPtr BlobBufferPointerFn(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate int GetBufferSizeDelegate(IntPtr self); + private delegate nint BlobBufferSizeFn(IntPtr self); [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate int ReleaseDelegate(IntPtr self); + private delegate int BlobReleaseFn(IntPtr self); private static byte[] CompileHlsl(string source, string entryPoint, string target) { - var hr = D3DCompile(source, source.Length, null, IntPtr.Zero, IntPtr.Zero, + var hr = D3DCompile(source, (nint)source.Length, null, IntPtr.Zero, IntPtr.Zero, entryPoint, target, 0, 0, out var codeBlob, out var errorBlob); if (hr != 0 || codeBlob == IntPtr.Zero) @@ -456,7 +432,7 @@ float4 main(PS_INPUT input) : SV_Target BlobRelease(errorBlob); var bufferPtr = BlobGetBufferPointer(codeBlob); - var bufferSize = BlobGetBufferSize(codeBlob); + var bufferSize = (int)BlobGetBufferSize(codeBlob); var result = new byte[bufferSize]; Marshal.Copy(bufferPtr, result, 0, bufferSize); BlobRelease(codeBlob); @@ -471,11 +447,13 @@ float4 main(PS_INPUT input) : SV_Target _fontTexture?.Dispose(); _fontTextureView?.Dispose(); _fontResourceSet?.Dispose(); + _projSamplerSet?.Dispose(); _pipeline?.Dispose(); - _layout?.Dispose(); + _projSamplerLayout?.Dispose(); _textureLayout?.Dispose(); - foreach (var shader in _shaders) - shader?.Dispose(); + if (_shaders is not null) + foreach (var shader in _shaders) + shader?.Dispose(); foreach (var rs in _resourceSets.Values) rs?.Dispose(); } diff --git a/src/Nexus.Simulator/Rendering/ViewTransform.cs b/src/Nexus.Simulator/Rendering/ViewTransform.cs new file mode 100644 index 0000000..4d947cb --- /dev/null +++ b/src/Nexus.Simulator/Rendering/ViewTransform.cs @@ -0,0 +1,62 @@ +using System.Numerics; + +namespace Nexus.Simulator.Rendering; + +/// +/// Encapsulates the 45° rotated isometric camera transform. +/// Grid coordinates are rotated so that WASD maps to screen up/left/down/right. +/// +public readonly struct ViewTransform +{ + private readonly Vector2 _canvasOrigin; + private readonly Vector2 _viewOffset; + private readonly float _zoom; + private readonly float _worldToGrid; + private const float C = 0.70710678f; // cos(45°) = 1/√2 + + public ViewTransform(Vector2 canvasOrigin, Vector2 viewOffset, float zoom, float worldToGrid) + { + _canvasOrigin = canvasOrigin; + _viewOffset = viewOffset; + _zoom = zoom; + _worldToGrid = worldToGrid; + } + + public float Zoom => _zoom; + public float WorldToGrid => _worldToGrid; + + /// + /// Scale factor from world units to screen pixels (distance-preserving rotation). + /// + public float WorldScale => _worldToGrid * _zoom; + + /// + /// Convert world position to screen position. + /// + public Vector2 WorldToScreen(Vector2 worldPos) + { + var gx = worldPos.X * _worldToGrid; + var gy = worldPos.Y * _worldToGrid; + return GridToScreen(gx, gy); + } + + /// + /// Convert absolute grid position to screen position. + /// + public Vector2 GridToScreen(float gx, float gy) + { + var rx = (gx - gy) * C; + var ry = -(gx + gy) * C; + return _canvasOrigin + _viewOffset + new Vector2(rx, ry) * _zoom; + } + + /// + /// Convert screen-relative position (relative to canvas origin) to absolute grid coords. + /// + public Vector2 ScreenToGrid(float sx, float sy) + { + var rx = (sx - _viewOffset.X) / _zoom; + var ry = (sy - _viewOffset.Y) / _zoom; + return new Vector2((rx - ry) * C, -(rx + ry) * C); + } +} diff --git a/src/Nexus.Simulator/World/SimEnemy.cs b/src/Nexus.Simulator/World/SimEnemy.cs index 95903b4..2984869 100644 --- a/src/Nexus.Simulator/World/SimEnemy.cs +++ b/src/Nexus.Simulator/World/SimEnemy.cs @@ -8,9 +8,16 @@ public enum EnemyAiState Idle, Chasing, Attacking, + Retreating, // Ranged enemy backing away to maintain distance Dead, } +public enum EnemyType +{ + Melee, + Ranged, +} + public class SimEnemy { private static uint _nextId = 1000; @@ -20,14 +27,22 @@ public class SimEnemy public int Health { get; set; } public int MaxHealth { get; set; } public MonsterRarity Rarity { get; set; } + public EnemyType Type { get; set; } public EnemyAiState AiState { get; set; } = EnemyAiState.Idle; public float MoveSpeed { get; set; } + // Damage (scaled by rarity) + public int AttackDamage { get; set; } + // Timers public float AttackCooldownRemaining { get; set; } public float DespawnTimer { get; set; } public float RespawnTimer { get; set; } + // Ranged-specific + public float PreferredRange { get; set; } // Distance ranged enemies try to maintain + public float AttackRange { get; set; } // Max attack range (melee=100, ranged=500) + // Wander public Vector2 WanderTarget { get; set; } public float WanderTimer { get; set; } @@ -35,13 +50,16 @@ public class SimEnemy public bool IsAlive => Health > 0; public bool IsAttacking => AiState == EnemyAiState.Attacking; + public bool IsRanged => Type == EnemyType.Ranged; - public SimEnemy(Vector2 position, MonsterRarity rarity, int baseHealth, float moveSpeed) + public SimEnemy(Vector2 position, MonsterRarity rarity, EnemyType type, + int baseHealth, int baseDamage, float moveSpeed) { Id = Interlocked.Increment(ref _nextId); Position = position; SpawnPosition = position; Rarity = rarity; + Type = type; MoveSpeed = moveSpeed; var hpMultiplier = rarity switch @@ -53,6 +71,15 @@ public class SimEnemy }; MaxHealth = (int)(baseHealth * hpMultiplier); Health = MaxHealth; + + var dmgMultiplier = rarity switch + { + MonsterRarity.Magic => 1.5f, + MonsterRarity.Rare => 2.5f, + MonsterRarity.Unique => 4f, + _ => 1f, + }; + AttackDamage = (int)(baseDamage * dmgMultiplier); } public void TakeDamage(int damage) diff --git a/src/Nexus.Simulator/World/SimPlayer.cs b/src/Nexus.Simulator/World/SimPlayer.cs index 1ca20ab..5b409e1 100644 --- a/src/Nexus.Simulator/World/SimPlayer.cs +++ b/src/Nexus.Simulator/World/SimPlayer.cs @@ -9,31 +9,44 @@ public class SimPlayer public int MaxHealth { get; set; } public int Mana { get; set; } public int MaxMana { get; set; } + public int Es { get; set; } + public int MaxEs { get; set; } public float MoveSpeed { get; set; } public float HealthRegen { get; set; } public float ManaRegen { get; set; } + public float EsRegen { get; set; } + public float EsRechargeDelay { get; set; } // Accumulate fractional regen private float _healthRegenAccum; private float _manaRegenAccum; + private float _esRegenAccum; + private float _timeSinceLastDamage; - public SimPlayer(int maxHealth, int maxMana, float moveSpeed, float healthRegen, float manaRegen) + public SimPlayer(int maxHealth, int maxMana, int maxEs, + float moveSpeed, float healthRegen, float manaRegen, + float esRegen, float esRechargeDelay) { MaxHealth = maxHealth; MaxMana = maxMana; + MaxEs = maxEs; Health = maxHealth; Mana = maxMana; + Es = maxEs; MoveSpeed = moveSpeed; HealthRegen = healthRegen; ManaRegen = manaRegen; + EsRegen = esRegen; + EsRechargeDelay = esRechargeDelay; + _timeSinceLastDamage = esRechargeDelay; // Start with ES recharging } public void Update(float dt) { - // Regenerate - _healthRegenAccum += HealthRegen * dt; - _manaRegenAccum += ManaRegen * dt; + _timeSinceLastDamage += dt; + // Health regen (always active) + _healthRegenAccum += HealthRegen * dt; if (_healthRegenAccum >= 1f) { var amount = (int)_healthRegenAccum; @@ -41,18 +54,58 @@ public class SimPlayer _healthRegenAccum -= amount; } + // Mana regen + _manaRegenAccum += ManaRegen * dt; if (_manaRegenAccum >= 1f) { var amount = (int)_manaRegenAccum; Mana = Math.Min(MaxMana, Mana + amount); _manaRegenAccum -= amount; } + + // ES recharge (after delay since last damage) + if (_timeSinceLastDamage >= EsRechargeDelay && Es < MaxEs) + { + _esRegenAccum += EsRegen * dt; + if (_esRegenAccum >= 1f) + { + var amount = (int)_esRegenAccum; + Es = Math.Min(MaxEs, Es + amount); + _esRegenAccum -= amount; + } + } } public void TakeDamage(int damage) { + _timeSinceLastDamage = 0f; + _esRegenAccum = 0f; + + // ES absorbs damage first + if (Es > 0) + { + if (damage <= Es) + { + Es -= damage; + return; + } + damage -= Es; + Es = 0; + } + Health = Math.Max(0, Health - damage); } public bool IsAlive => Health > 0; + + /// Effective HP percentage considering both ES and Life. + public float EffectiveHpPercent + { + get + { + var totalMax = MaxHealth + MaxEs; + if (totalMax == 0) return 0f; + return (float)(Health + Es) / totalMax * 100f; + } + } } diff --git a/src/Nexus.Simulator/World/SimProjectile.cs b/src/Nexus.Simulator/World/SimProjectile.cs index b4199b0..5b783f2 100644 --- a/src/Nexus.Simulator/World/SimProjectile.cs +++ b/src/Nexus.Simulator/World/SimProjectile.cs @@ -12,8 +12,10 @@ public class SimProjectile public int Damage { get; set; } public float DistanceTraveled { get; set; } public bool IsExpired { get; set; } + public bool IsEnemyProjectile { get; set; } - public SimProjectile(Vector2 origin, Vector2 direction, float speed, float maxRange, float hitRadius, int damage) + public SimProjectile(Vector2 origin, Vector2 direction, float speed, float maxRange, float hitRadius, int damage, + bool isEnemyProjectile = false) { Position = origin; Direction = Vector2.Normalize(direction); @@ -21,6 +23,7 @@ public class SimProjectile MaxRange = maxRange; HitRadius = hitRadius; Damage = damage; + IsEnemyProjectile = isEnemyProjectile; } public void Update(float dt) diff --git a/src/Nexus.Simulator/World/SimWorld.cs b/src/Nexus.Simulator/World/SimWorld.cs index a35796d..1c9f858 100644 --- a/src/Nexus.Simulator/World/SimWorld.cs +++ b/src/Nexus.Simulator/World/SimWorld.cs @@ -1,6 +1,7 @@ using System.Numerics; using Nexus.Core; using Nexus.Simulator.Config; +using Serilog; namespace Nexus.Simulator.World; @@ -33,8 +34,9 @@ public class SimWorld var gridToWorld = 1f / config.WorldToGrid; var (sx, sy) = TerrainGenerator.FindSpawnPosition(Terrain); Player = new SimPlayer( - config.PlayerMaxHealth, config.PlayerMaxMana, - config.PlayerMoveSpeed, config.PlayerHealthRegen, config.PlayerManaRegen) + config.PlayerMaxHealth, config.PlayerMaxMana, config.PlayerMaxEs, + config.PlayerMoveSpeed, config.PlayerHealthRegen, config.PlayerManaRegen, + config.PlayerEsRegen, config.PlayerEsRechargeDelay) { Position = new Vector2(sx * gridToWorld, sy * gridToWorld), }; @@ -51,6 +53,7 @@ public class SimWorld Player.Position = new Vector2(sx * gridToWorld, sy * gridToWorld); Player.Health = Player.MaxHealth; Player.Mana = Player.MaxMana; + Player.Es = Player.MaxEs; Enemies.Clear(); Projectiles.Clear(); ActiveEffects.Clear(); @@ -70,13 +73,16 @@ public class SimWorld dt *= _config.SpeedMultiplier; TickNumber++; + // 0. Expand terrain if player near edge + CheckAndExpandTerrain(); + // 1. Move player MovePlayer(dt); // 2. Process queued skills ProcessSkills(); - // 3. Update projectiles + // 3. Update projectiles (player + enemy) UpdateProjectiles(dt); // 4. Update skill effects @@ -92,45 +98,62 @@ public class SimWorld Player.Update(dt); } + // Pre-computed 8 cardinal directions for fallback movement + private static readonly Vector2[] Cardinals = + [ + new(1, 0), new(-1, 0), new(0, 1), new(0, -1), + Vector2.Normalize(new(1, 1)), Vector2.Normalize(new(1, -1)), + Vector2.Normalize(new(-1, 1)), Vector2.Normalize(new(-1, -1)), + ]; + private void MovePlayer(float dt) { if (MoveDirection.LengthSquared() < 0.001f) return; var dir = Vector2.Normalize(MoveDirection); - var newPos = Player.Position + dir * Player.MoveSpeed * dt; + var step = Player.MoveSpeed * dt; - // Terrain collision + // Try full direction + if (TryMove(dir, step)) return; + + // Try axis-aligned slides + if (MathF.Abs(dir.X) > 0.01f && TryMove(new Vector2(dir.X > 0 ? 1 : -1, 0), step)) return; + if (MathF.Abs(dir.Y) > 0.01f && TryMove(new Vector2(0, dir.Y > 0 ? 1 : -1), step)) return; + + // Try perpendicular directions (wall slide along corners) + var perp1 = new Vector2(-dir.Y, dir.X); + var perp2 = new Vector2(dir.Y, -dir.X); + if (TryMove(perp1, step * 0.5f)) return; + if (TryMove(perp2, step * 0.5f)) return; + + // All preferred directions failed — try all 8 cardinals at reduced step + // Sort by dot product with desired direction (prefer closest to intended direction) + foreach (var cardinal in Cardinals) + { + if (TryMove(cardinal, step * 0.5f)) return; + } + + // Absolute last resort: try tiny steps in all 8 directions to nudge out + foreach (var cardinal in Cardinals) + { + if (TryMove(cardinal, step * 0.15f)) return; + } + } + + private bool TryMove(Vector2 dir, float step) + { + var newPos = Player.Position + dir * step; var gx = (int)(newPos.X * _config.WorldToGrid); var gy = (int)(newPos.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(gx, gy)) - Player.Position = newPos; - else - { - // Try sliding along X - var slideX = new Vector2(Player.Position.X + dir.X * Player.MoveSpeed * dt, Player.Position.Y); - var sgx = (int)(slideX.X * _config.WorldToGrid); - var sgy = (int)(slideX.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(sgx, sgy)) - { - Player.Position = slideX; - return; - } - - // Try sliding along Y - var slideY = new Vector2(Player.Position.X, Player.Position.Y + dir.Y * Player.MoveSpeed * dt); - sgx = (int)(slideY.X * _config.WorldToGrid); - sgy = (int)(slideY.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(sgx, sgy)) - Player.Position = slideY; - } + if (!Terrain.IsWalkable(gx, gy)) return false; + Player.Position = newPos; + return true; } private void ProcessSkills() { while (_skillQueue.TryDequeue(out var skill)) { - // Determine skill type based on scan code slot - // Slots 0-1 (LMB/RMB) = melee, 3 (Q) = AOE, 4 (E) = projectile, else = melee var type = GetSkillType(skill.scanCode); var targetPos = skill.targetWorldPos; @@ -196,6 +219,8 @@ public class SimWorld } enemy.TakeDamage(_config.SkillBaseDamage); + if (!enemy.IsAlive) + Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); } } @@ -211,12 +236,15 @@ public class SimWorld }; ActiveEffects.Add(effect); - // Apply AOE damage foreach (var enemy in Enemies) { if (!enemy.IsAlive) continue; if (Vector2.Distance(enemy.Position, targetPos) <= _config.AoeRadius) + { enemy.TakeDamage(_config.SkillBaseDamage); + if (!enemy.IsAlive) + Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); + } } } @@ -226,12 +254,9 @@ public class SimWorld if (dir.LengthSquared() < 1f) dir = Vector2.UnitX; var projectile = new SimProjectile( - Player.Position, - dir, - _config.ProjectileSpeed, - _config.ProjectileRange, - _config.ProjectileHitRadius, - _config.SkillBaseDamage); + Player.Position, dir, + _config.ProjectileSpeed, _config.ProjectileRange, + _config.ProjectileHitRadius, _config.SkillBaseDamage); Projectiles.Add(projectile); @@ -261,17 +286,35 @@ public class SimWorld proj.IsExpired = true; } - // Check enemy hits if (!proj.IsExpired) { - foreach (var enemy in Enemies) + if (proj.IsEnemyProjectile) { - if (!enemy.IsAlive) continue; - if (Vector2.Distance(enemy.Position, proj.Position) <= proj.HitRadius) + // Enemy projectile hits player + if (Vector2.Distance(Player.Position, proj.Position) <= proj.HitRadius) { - enemy.TakeDamage(proj.Damage); + Player.TakeDamage(proj.Damage); + Log.Information("Damage: -{Dmg} projectile HP={HP}/{MaxHP} ES={ES}/{MaxES}", + proj.Damage, Player.Health, Player.MaxHealth, Player.Es, Player.MaxEs); + if (!Player.IsAlive) + Log.Warning("PLAYER DIED to projectile"); proj.IsExpired = true; - break; + } + } + else + { + // Player projectile hits enemies + foreach (var enemy in Enemies) + { + if (!enemy.IsAlive) continue; + if (Vector2.Distance(enemy.Position, proj.Position) <= proj.HitRadius) + { + enemy.TakeDamage(proj.Damage); + if (!enemy.IsAlive) + Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); + proj.IsExpired = true; + break; + } } } } @@ -302,7 +345,6 @@ public class SimWorld enemy.DespawnTimer -= dt; if (enemy.DespawnTimer <= 0) { - // Queue respawn _respawnQueue.Add((_config.EnemyRespawnTime, enemy.Rarity)); Enemies.RemoveAt(i); } @@ -315,53 +357,148 @@ public class SimWorld var dist = Vector2.Distance(enemy.Position, Player.Position); - if (dist <= _config.EnemyAttackRange && Player.IsAlive) + if (!Player.IsAlive) { - // In attack range - enemy.AiState = EnemyAiState.Attacking; - if (enemy.AttackCooldownRemaining <= 0) - { - Player.TakeDamage(_config.EnemyAttackDamage); - enemy.AttackCooldownRemaining = _config.EnemyAttackCooldown; - } - } - else if (dist <= _config.EnemyAggroRange && Player.IsAlive) - { - // Chase player - enemy.AiState = EnemyAiState.Chasing; - var dir = Vector2.Normalize(Player.Position - enemy.Position); - var newPos = enemy.Position + dir * enemy.MoveSpeed * dt; - - var gx = (int)(newPos.X * _config.WorldToGrid); - var gy = (int)(newPos.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(gx, gy)) - enemy.Position = newPos; - } - else - { - // Idle: random wander enemy.AiState = EnemyAiState.Idle; - enemy.WanderTimer -= dt; - if (enemy.WanderTimer <= 0) - { - // Pick new wander target - var angle = _rng.NextSingle() * MathF.Tau; - var dist2 = _rng.NextSingle() * _config.EnemyWanderRadius; - enemy.WanderTarget = enemy.SpawnPosition + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist2; - enemy.WanderTimer = 2f + _rng.NextSingle() * 3f; - } + UpdateWander(enemy, dt); + continue; + } - if (Vector2.Distance(enemy.Position, enemy.WanderTarget) > 10f) - { - var dir = Vector2.Normalize(enemy.WanderTarget - enemy.Position); - var newPos = enemy.Position + dir * enemy.MoveSpeed * 0.3f * dt; - var gx = (int)(newPos.X * _config.WorldToGrid); - var gy = (int)(newPos.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(gx, gy)) - enemy.Position = newPos; - } + if (enemy.IsRanged) + UpdateRangedEnemy(enemy, dist, dt); + else + UpdateMeleeEnemy(enemy, dist, dt); + } + } + + private void UpdateMeleeEnemy(SimEnemy enemy, float dist, float dt) + { + if (dist <= enemy.AttackRange) + { + // In melee range — attack + enemy.AiState = EnemyAiState.Attacking; + if (enemy.AttackCooldownRemaining <= 0) + { + Player.TakeDamage(enemy.AttackDamage); + Log.Information("Damage: -{Dmg} melee ({Rarity}) HP={HP}/{MaxHP} ES={ES}/{MaxES}", + enemy.AttackDamage, enemy.Rarity, Player.Health, Player.MaxHealth, Player.Es, Player.MaxEs); + if (!Player.IsAlive) + Log.Warning("PLAYER DIED to melee {Rarity} #{Id}", enemy.Rarity, enemy.Id); + enemy.AttackCooldownRemaining = _config.EnemyMeleeAttackCooldown; } } + else if (dist <= _config.EnemyAggroRange) + { + // Chase player + enemy.AiState = EnemyAiState.Chasing; + MoveToward(enemy, Player.Position, enemy.MoveSpeed, dt); + } + else + { + enemy.AiState = EnemyAiState.Idle; + UpdateWander(enemy, dt); + } + } + + private void UpdateRangedEnemy(SimEnemy enemy, float dist, float dt) + { + if (dist > _config.EnemyAggroRange) + { + // Out of aggro range — idle wander + enemy.AiState = EnemyAiState.Idle; + UpdateWander(enemy, dt); + return; + } + + if (dist <= enemy.AttackRange && enemy.AttackCooldownRemaining <= 0) + { + // In range and ready to fire — shoot projectile + enemy.AiState = EnemyAiState.Attacking; + FireEnemyProjectile(enemy); + enemy.AttackCooldownRemaining = _config.EnemyRangedAttackCooldown; + return; + } + + // Too close — retreat to preferred range + if (dist < enemy.PreferredRange * 0.7f) + { + enemy.AiState = EnemyAiState.Retreating; + MoveAwayFrom(enemy, Player.Position, enemy.MoveSpeed, dt); + return; + } + + // Too far — close in to attack range + if (dist > enemy.AttackRange) + { + enemy.AiState = EnemyAiState.Chasing; + MoveToward(enemy, Player.Position, enemy.MoveSpeed * 0.8f, dt); + return; + } + + // At good range, waiting for cooldown — strafe laterally + enemy.AiState = EnemyAiState.Chasing; + var toPlayer = Vector2.Normalize(Player.Position - enemy.Position); + var strafe = new Vector2(-toPlayer.Y, toPlayer.X); // perpendicular + // Alternate strafe direction based on enemy ID + if (enemy.Id % 2 == 0) strafe = -strafe; + MoveToward(enemy, enemy.Position + strafe * 100f, enemy.MoveSpeed * 0.5f, dt); + } + + private void FireEnemyProjectile(SimEnemy enemy) + { + var dir = Player.Position - enemy.Position; + if (dir.LengthSquared() < 1f) return; + + var proj = new SimProjectile( + enemy.Position, dir, + _config.EnemyProjectileSpeed, + _config.EnemyRangedAttackRange * 1.5f, + _config.EnemyProjectileHitRadius, + enemy.AttackDamage, + isEnemyProjectile: true); + + Projectiles.Add(proj); + } + + private void MoveToward(SimEnemy enemy, Vector2 target, float speed, float dt) + { + var dir = target - enemy.Position; + if (dir.LengthSquared() < 1f) return; + dir = Vector2.Normalize(dir); + + var newPos = enemy.Position + dir * speed * dt; + var gx = (int)(newPos.X * _config.WorldToGrid); + var gy = (int)(newPos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + enemy.Position = newPos; + } + + private void MoveAwayFrom(SimEnemy enemy, Vector2 target, float speed, float dt) + { + var dir = enemy.Position - target; + if (dir.LengthSquared() < 1f) return; + dir = Vector2.Normalize(dir); + + var newPos = enemy.Position + dir * speed * dt; + var gx = (int)(newPos.X * _config.WorldToGrid); + var gy = (int)(newPos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + enemy.Position = newPos; + } + + private void UpdateWander(SimEnemy enemy, float dt) + { + enemy.WanderTimer -= dt; + if (enemy.WanderTimer <= 0) + { + var angle = _rng.NextSingle() * MathF.Tau; + var dist2 = _rng.NextSingle() * _config.EnemyWanderRadius; + enemy.WanderTarget = enemy.SpawnPosition + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist2; + enemy.WanderTimer = 2f + _rng.NextSingle() * 3f; + } + + if (Vector2.Distance(enemy.Position, enemy.WanderTarget) > 10f) + MoveToward(enemy, enemy.WanderTarget, enemy.MoveSpeed * 0.3f, dt); } private void UpdateRespawns(float dt) @@ -371,54 +508,131 @@ public class SimWorld var (timer, rarity) = _respawnQueue[i]; timer -= dt; if (timer <= 0) - { - SpawnEnemy(rarity); _respawnQueue.RemoveAt(i); - } else - { _respawnQueue[i] = (timer, rarity); - } + } + + // Cull enemies too far from player + for (var i = Enemies.Count - 1; i >= 0; i--) + { + var dist = Vector2.Distance(Enemies[i].Position, Player.Position); + if (dist > _config.EnemyCullDist) + Enemies.RemoveAt(i); } // Maintain population - var aliveCount = Enemies.Count(e => e.IsAlive) + _respawnQueue.Count; + var aliveCount = Enemies.Count(e => e.IsAlive); while (aliveCount < _config.TargetEnemyCount) { - SpawnEnemy(RollRarity()); - aliveCount++; + var spawned = SpawnGroup(RollRarity()); + aliveCount += spawned; + if (spawned == 0) break; } } public void SpawnEnemyAt(Vector2 worldPos, MonsterRarity rarity) { - var enemy = new SimEnemy(worldPos, rarity, _config.EnemyBaseHealth, - _config.PlayerMoveSpeed * _config.EnemyMoveSpeedFactor) + var type = _rng.NextSingle() < _config.RangedEnemyChance ? EnemyType.Ranged : EnemyType.Melee; + var baseDmg = type == EnemyType.Ranged ? _config.EnemyRangedBaseDamage : _config.EnemyMeleeBaseDamage; + + var enemy = new SimEnemy(worldPos, rarity, type, + _config.EnemyBaseHealth, baseDmg, + Player.MoveSpeed * _config.EnemyMoveSpeedFactor) { WanderTarget = worldPos, WanderTimer = _rng.NextSingle() * 3f, + AttackRange = type == EnemyType.Ranged ? _config.EnemyRangedAttackRange : _config.EnemyMeleeAttackRange, + PreferredRange = type == EnemyType.Ranged ? _config.EnemyRangedPreferredRange : 0f, }; Enemies.Add(enemy); } private void SpawnEnemies(int count) { - for (var i = 0; i < count; i++) - SpawnEnemy(RollRarity()); + while (count > 0) + { + var spawned = SpawnGroup(RollRarity()); + if (spawned == 0) break; + count -= spawned; + } } - private void SpawnEnemy(MonsterRarity rarity) + private int SpawnGroup(MonsterRarity leaderRarity) { - var gridToWorld = 1f / _config.WorldToGrid; - var pos = TerrainGenerator.FindRandomWalkable(Terrain, _rng); - if (pos is null) return; + var center = FindSpawnNearPlayer(); + if (center is null) return 0; - var worldPos = new Vector2(pos.Value.x * gridToWorld, pos.Value.y * gridToWorld); + var groupSize = _rng.Next(_config.EnemyGroupMin, _config.EnemyGroupMax + 1); + var spawned = 0; - // Don't spawn too close to player - if (Vector2.Distance(worldPos, Player.Position) < 300f) return; + for (var i = 0; i < groupSize; i++) + { + var rarity = i == 0 ? leaderRarity : MonsterRarity.White; + var offset = i == 0 + ? Vector2.Zero + : new Vector2( + (_rng.NextSingle() - 0.5f) * 2f * _config.EnemyGroupSpread, + (_rng.NextSingle() - 0.5f) * 2f * _config.EnemyGroupSpread); - SpawnEnemyAt(worldPos, rarity); + var pos = center.Value + offset; + var gx = (int)(pos.X * _config.WorldToGrid); + var gy = (int)(pos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + { + SpawnEnemyAt(pos, rarity); + spawned++; + } + } + + return spawned; + } + + private Vector2? FindSpawnNearPlayer() + { + var baseAngle = MoveDirection.LengthSquared() > 0.01f + ? MathF.Atan2(MoveDirection.Y, MoveDirection.X) + : _rng.NextSingle() * MathF.Tau; + + for (var attempt = 0; attempt < 30; attempt++) + { + var angle = baseAngle + (_rng.NextSingle() - 0.5f) * MathF.PI; + var dist = _config.EnemySpawnMinDist + _rng.NextSingle() * (_config.EnemySpawnMaxDist - _config.EnemySpawnMinDist); + var pos = Player.Position + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist; + + var gx = (int)(pos.X * _config.WorldToGrid); + var gy = (int)(pos.Y * _config.WorldToGrid); + if (Terrain.IsWalkable(gx, gy)) + return pos; + } + + return null; + } + + private void CheckAndExpandTerrain() + { + var gx = (int)(Player.Position.X * _config.WorldToGrid); + var gy = (int)(Player.Position.Y * _config.WorldToGrid); + var t = Terrain; + + var distLeft = gx - t.OffsetX; + var distRight = (t.OffsetX + t.Width - 1) - gx; + var distTop = gy - t.OffsetY; + var distBottom = (t.OffsetY + t.Height - 1) - gy; + + var amt = _config.ExpandAmount; + var expandLeft = distLeft < _config.ExpandThreshold ? amt : 0; + var expandRight = distRight < _config.ExpandThreshold ? amt : 0; + var expandTop = distTop < _config.ExpandThreshold ? amt : 0; + var expandBottom = distBottom < _config.ExpandThreshold ? amt : 0; + + if (expandLeft > 0 || expandRight > 0 || expandTop > 0 || expandBottom > 0) + { + Terrain = TerrainGenerator.Expand(Terrain, expandLeft, expandRight, expandTop, expandBottom, _rng); + Serilog.Log.Information( + "Terrain expanded: {W}x{H} offset=({Ox},{Oy})", + Terrain.Width, Terrain.Height, Terrain.OffsetX, Terrain.OffsetY); + } } private MonsterRarity RollRarity() diff --git a/src/Nexus.Simulator/World/TerrainGenerator.cs b/src/Nexus.Simulator/World/TerrainGenerator.cs index 548f19c..950b64b 100644 --- a/src/Nexus.Simulator/World/TerrainGenerator.cs +++ b/src/Nexus.Simulator/World/TerrainGenerator.cs @@ -1,103 +1,189 @@ +using System.Numerics; using Nexus.Core; namespace Nexus.Simulator.World; public static class TerrainGenerator { - private record Room(int X, int Y, int Width, int Height) + // Permutation table for Perlin noise (fixed seed for deterministic terrain) + private static readonly int[] Perm; + + static TerrainGenerator() { - public int CenterX => X + Width / 2; - public int CenterY => Y + Height / 2; + var p = new int[256]; + for (var i = 0; i < 256; i++) p[i] = i; + var rng = new Random(42); + for (var i = 255; i > 0; i--) + { + var j = rng.Next(i + 1); + (p[i], p[j]) = (p[j], p[i]); + } + Perm = new int[512]; + for (var i = 0; i < 512; i++) Perm[i] = p[i & 255]; } public static WalkabilitySnapshot Generate(int width, int height, int? seed = null) { - var rng = seed.HasValue ? new Random(seed.Value) : new Random(); - var data = new byte[width * height]; // 0 = wall by default + var data = new byte[width * height]; + FillNoiseRegion(data, width, height, 0, 0, 0, 0, width, height); - var rooms = new List(); - var attempts = 0; - var targetRooms = 15 + rng.Next(10); + // Clear spawn area at center + FillCircle(data, width, height, width / 2, height / 2, 20, 1); - while (rooms.Count < targetRooms && attempts < 500) - { - attempts++; - var rw = rng.Next(20, 60); - var rh = rng.Next(20, 60); - var rx = rng.Next(2, width - rw - 2); - var ry = rng.Next(2, height - rh - 2); + return new WalkabilitySnapshot { Width = width, Height = height, Data = data }; + } - var candidate = new Room(rx, ry, rw, rh); + /// + /// Expands the terrain by adding cells on each side. Old data is preserved. + /// New regions use the same Perlin noise with absolute coordinates for seamless tiling. + /// + public static WalkabilitySnapshot Expand(WalkabilitySnapshot old, int left, int right, int top, int bottom, Random? rng = null) + { + var newW = old.Width + left + right; + var newH = old.Height + top + bottom; + var data = new byte[newW * newH]; + var newOffsetX = old.OffsetX - left; + var newOffsetY = old.OffsetY - top; - // Check overlap with existing rooms (with margin) - var overlaps = false; - foreach (var existing in rooms) - { - if (candidate.X - 3 < existing.X + existing.Width && - candidate.X + candidate.Width + 3 > existing.X && - candidate.Y - 3 < existing.Y + existing.Height && - candidate.Y + candidate.Height + 3 > existing.Y) - { - overlaps = true; - break; - } - } + // Fill entire new grid with noise (absolute coordinates ensure seamless expansion) + FillNoiseRegion(data, newW, newH, newOffsetX, newOffsetY, 0, 0, newW, newH); - if (!overlaps) - rooms.Add(candidate); - } - - // Carve rooms - foreach (var room in rooms) - { - for (var y = room.Y; y < room.Y + room.Height; y++) - for (var x = room.X; x < room.X + room.Width; x++) - data[y * width + x] = 1; - } - - // Connect rooms with corridors - for (var i = 1; i < rooms.Count; i++) - { - var a = rooms[i - 1]; - var b = rooms[i]; - CarveCorridorL(data, width, a.CenterX, a.CenterY, b.CenterX, b.CenterY, rng); - } - - // Also connect last to first for a loop - if (rooms.Count > 2) - { - var first = rooms[0]; - var last = rooms[^1]; - CarveCorridorL(data, width, first.CenterX, first.CenterY, last.CenterX, last.CenterY, rng); - } - - // Add some random extra connections - var extraConnections = rng.Next(3, 7); - for (var i = 0; i < extraConnections; i++) - { - var a = rooms[rng.Next(rooms.Count)]; - var b = rooms[rng.Next(rooms.Count)]; - if (a != b) - CarveCorridorL(data, width, a.CenterX, a.CenterY, b.CenterX, b.CenterY, rng); - } + // Overwrite the old region with preserved data + for (var y = 0; y < old.Height; y++) + Array.Copy(old.Data, y * old.Width, data, (y + top) * newW + left, old.Width); return new WalkabilitySnapshot { - Width = width, - Height = height, + Width = newW, + Height = newH, Data = data, + OffsetX = newOffsetX, + OffsetY = newOffsetY, }; } /// - /// Finds a walkable position near the center of the terrain. + /// Fills a region of the data array using Perlin noise at absolute grid coordinates. + /// Combines corridor noise (abs of fractal noise — zero crossings form paths) + /// with room noise (low-frequency — creates larger open areas). + /// + private static void FillNoiseRegion(byte[] data, int dataWidth, int dataHeight, + int offsetX, int offsetY, int regionX, int regionY, int regionW, int regionH) + { + // Corridor noise: abs(fractal) creates paths along zero crossings + const float corridorScale = 0.04f; + const float corridorThreshold = 0.28f; // Width of corridors (wider = more open) + + // Room noise: low-frequency blobs create open areas + const float roomScale = 0.015f; + const float roomThreshold = 0.10f; // Lower = larger rooms + + // Detail noise: adds roughness to walls + const float detailScale = 0.12f; + const float detailWeight = 0.03f; + + for (var ly = regionY; ly < regionY + regionH; ly++) + for (var lx = regionX; lx < regionX + regionW; lx++) + { + if (lx < 0 || lx >= dataWidth || ly < 0 || ly >= dataHeight) continue; + + float ax = lx + offsetX; + float ay = ly + offsetY; + + // Corridor network: abs creates linear paths along noise zero-crossings + var corridor = MathF.Abs(FractalNoise(ax * corridorScale, ay * corridorScale, 3)); + + // Rooms: large smooth blobs (offset coordinates to decorrelate from corridors) + var room = FractalNoise(ax * roomScale + 500f, ay * roomScale + 500f, 2); + + // Detail: roughen edges + var detail = Perlin(ax * detailScale + 200f, ay * detailScale + 200f) * detailWeight; + + // Walkable if on a corridor path OR inside a room + var walkable = (corridor + detail) < corridorThreshold || room > roomThreshold; + + data[ly * dataWidth + lx] = walkable ? (byte)1 : (byte)0; + } + } + + /// + /// Multi-octave fractal Brownian motion noise. + /// + private static float FractalNoise(float x, float y, int octaves) + { + var value = 0f; + var amplitude = 1f; + var freq = 1f; + var maxAmp = 0f; + + for (var i = 0; i < octaves; i++) + { + value += Perlin(x * freq, y * freq) * amplitude; + maxAmp += amplitude; + amplitude *= 0.5f; + freq *= 2f; + } + + return value / maxAmp; + } + + /// + /// Classic 2D Perlin noise. Returns values roughly in [-1, 1]. + /// + private static float Perlin(float x, float y) + { + var xi = (int)MathF.Floor(x) & 255; + var yi = (int)MathF.Floor(y) & 255; + var xf = x - MathF.Floor(x); + var yf = y - MathF.Floor(y); + + var u = Fade(xf); + var v = Fade(yf); + + var aa = Perm[Perm[xi] + yi]; + var ab = Perm[Perm[xi] + yi + 1]; + var ba = Perm[Perm[xi + 1] + yi]; + var bb = Perm[Perm[xi + 1] + yi + 1]; + + var x1 = Lerp(Grad(aa, xf, yf), Grad(ba, xf - 1, yf), u); + var x2 = Lerp(Grad(ab, xf, yf - 1), Grad(bb, xf - 1, yf - 1), u); + return Lerp(x1, x2, v); + } + + private static float Fade(float t) => t * t * t * (t * (t * 6 - 15) + 10); + + private static float Lerp(float a, float b, float t) => a + t * (b - a); + + private static float Grad(int hash, float x, float y) + { + var h = hash & 3; + var u = h < 2 ? x : y; + var v = h < 2 ? y : x; + return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); + } + + private static void FillCircle(byte[] data, int width, int height, int cx, int cy, int radius, byte value) + { + var r2 = radius * radius; + for (var dy = -radius; dy <= radius; dy++) + for (var dx = -radius; dx <= radius; dx++) + { + if (dx * dx + dy * dy > r2) continue; + var x = cx + dx; + var y = cy + dy; + if (x >= 0 && x < width && y >= 0 && y < height) + data[y * width + x] = value; + } + } + + /// + /// Finds a walkable position near the center. Returns absolute grid coords. /// public static (int x, int y) FindSpawnPosition(WalkabilitySnapshot terrain) { - var cx = terrain.Width / 2; - var cy = terrain.Height / 2; + var cx = terrain.OffsetX + terrain.Width / 2; + var cy = terrain.OffsetY + terrain.Height / 2; - // Spiral outward for (var r = 0; r < Math.Max(terrain.Width, terrain.Height); r++) { for (var dx = -r; dx <= r; dx++) @@ -115,61 +201,19 @@ public static class TerrainGenerator } /// - /// Finds a random walkable position. + /// Finds a random walkable position. Returns absolute grid coords. /// public static (int x, int y)? FindRandomWalkable(WalkabilitySnapshot terrain, Random rng, int maxAttempts = 200) { for (var i = 0; i < maxAttempts; i++) { - var x = rng.Next(terrain.Width); - var y = rng.Next(terrain.Height); - if (terrain.IsWalkable(x, y)) - return (x, y); + var lx = rng.Next(terrain.Width); + var ly = rng.Next(terrain.Height); + var ax = lx + terrain.OffsetX; + var ay = ly + terrain.OffsetY; + if (terrain.IsWalkable(ax, ay)) + return (ax, ay); } return null; } - - private static void CarveCorridorL(byte[] data, int width, int x1, int y1, int x2, int y2, Random rng) - { - var corridorWidth = 2 + rng.Next(2); - - // L-shaped: horizontal then vertical (or vice versa) - if (rng.Next(2) == 0) - { - CarveHorizontal(data, width, x1, x2, y1, corridorWidth); - CarveVertical(data, width, y1, y2, x2, corridorWidth); - } - else - { - CarveVertical(data, width, y1, y2, x1, corridorWidth); - CarveHorizontal(data, width, x1, x2, y2, corridorWidth); - } - } - - private static void CarveHorizontal(byte[] data, int width, int x1, int x2, int y, int thickness) - { - var xMin = Math.Min(x1, x2); - var xMax = Math.Max(x1, x2); - for (var x = xMin; x <= xMax; x++) - for (var dy = 0; dy < thickness; dy++) - { - var ry = y + dy; - if (ry >= 0 && ry < data.Length / width && x >= 0 && x < width) - data[ry * width + x] = 1; - } - } - - private static void CarveVertical(byte[] data, int width, int y1, int y2, int x, int thickness) - { - var height = data.Length / width; - var yMin = Math.Min(y1, y2); - var yMax = Math.Max(y1, y2); - for (var y = yMin; y <= yMax; y++) - for (var dx = 0; dx < thickness; dx++) - { - var rx = x + dx; - if (y >= 0 && y < height && rx >= 0 && rx < width) - data[y * width + rx] = 1; - } - } } diff --git a/src/Nexus.Systems/AreaProgressionSystem.cs b/src/Nexus.Systems/AreaProgressionSystem.cs new file mode 100644 index 0000000..5295890 --- /dev/null +++ b/src/Nexus.Systems/AreaProgressionSystem.cs @@ -0,0 +1,582 @@ +using System.Numerics; +using Nexus.Core; +using Nexus.Data; +using Nexus.Pathfinding; +using Serilog; + +namespace Nexus.Systems; + +public sealed class AreaProgressionSystem : ISystem +{ + private enum Phase { Exploring, Looting, NavigatingToChest, InteractingChest, NavigatingToTransition, Interacting, TalkingToNpc } + + private readonly AreaGraph _graph; + private readonly NavigationController _nav; + private readonly BotConfig _config; + + private readonly HashSet _visitedAreas = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _blacklistedTransitions = new(); + private string? _currentAreaId; + private string? _targetAreaId; + private string? _targetTransitionName; + private uint _targetTransitionEntityId; + private Phase _phase = Phase.Exploring; + private uint _lastAreaHash; + private long _lastTransitionCheckMs; + private long _interactStartMs; + private uint _lootTargetEntityId; + private long _lootStartMs; + private uint _chestTargetEntityId; + private long _chestInteractStartMs; + private readonly HashSet _interactedChests = new(); + private bool _currentAreaExplored; + private bool _questDriven; + + public int Priority => SystemPriority.Navigation - 1; + public string Name => "Progression"; + public bool IsEnabled { get; set; } = true; + + public string? CurrentAreaId => _currentAreaId; + public string? TargetAreaId => _targetAreaId; + public IReadOnlySet VisitedAreas => _visitedAreas; + public string PhaseName => _phase.ToString(); + public string? TargetTransitionName => _targetTransitionName; + public bool IsLootingActive => _phase == Phase.Looting; + public bool IsQuestDriven => _questDriven; + public string? QuestTargetName => _targetAreaId is not null ? _graph.GetById(_targetAreaId)?.Name : null; + + public AreaProgressionSystem(BotConfig config, NavigationController nav, AreaGraph graph) + { + _config = config; + _nav = nav; + _graph = graph; + } + + public void Update(GameState state, ActionQueue actions, MovementBlender movement) + { + // Need area name from log watcher + if (state.CurrentAreaName is null) return; + + // Resolve current area + var node = _graph.GetByName(state.CurrentAreaName); + if (node is null) return; + + var prevAreaId = _currentAreaId; + _currentAreaId = node.Id; + _visitedAreas.Add(node.Id); + + var areaChanged = prevAreaId != _currentAreaId; + + if (areaChanged) + { + Log.Information("Progression: entered {Area} ({Id})", node.Name, node.Id); + + // Auto-mark all earlier areas (same act, lower order) as visited + foreach (var earlierId in _graph.GetEarlierAreas(_currentAreaId)) + _visitedAreas.Add(earlierId); + + // Force target recalculation + _targetAreaId = null; + } + + // Detect area hash change → reset navigation/phase state + if (state.AreaHash != _lastAreaHash) + { + _lastAreaHash = state.AreaHash; + _targetTransitionEntityId = 0; + _lootTargetEntityId = 0; + _chestTargetEntityId = 0; + _phase = Phase.Exploring; + _blacklistedTransitions.Clear(); + _interactedChests.Clear(); + } + + // Resolve target BEFORE deciding whether to explore — we need to know + // if the current area IS the target (affects exploration decision). + if (_targetAreaId is null) + { + _targetAreaId = FindQuestTarget(state); + _questDriven = _targetAreaId is not null; + _targetAreaId ??= _graph.FindNextTarget(_currentAreaId, _visitedAreas); + + if (_targetAreaId is null) + { + Log.Information("Progression: all reachable areas visited, no quest targets"); + IsEnabled = false; + return; + } + + var path = _graph.FindAreaPath(_currentAreaId, _targetAreaId); + if (path is null || path.Count < 2) + { + // Current area IS the target — no hop needed, just explore it + if (string.Equals(_currentAreaId, _targetAreaId, StringComparison.OrdinalIgnoreCase)) + { + Log.Information("Progression: arrived at target {Target}, exploring", + _graph.GetById(_targetAreaId)?.Name); + _targetTransitionName = null; + } + else + { + Log.Warning("Progression: no path from {From} to {To}", _currentAreaId, _targetAreaId); + _targetAreaId = null; + return; + } + } + else + { + var firstHop = path[1]; + var hopNode = _graph.GetById(firstHop); + _targetTransitionName = hopNode?.Name; + Log.Information("Progression: targeting {Target} via {Hop} ({Source})", + _graph.GetById(_targetAreaId)?.Name, _targetTransitionName, + _questDriven ? "quest" : "order"); + } + } + + // Now decide exploration — target is resolved so we know if we're at the destination + if (areaChanged) + { + var isAtTarget = string.Equals(_currentAreaId, _targetAreaId, StringComparison.OrdinalIgnoreCase); + // Towns: skip exploration. Pass-through areas (quest-driven, not final target): skip. + // Final target or non-quest areas: explore. + _currentAreaExplored = node.IsTown || (_questDriven && !isAtTarget); + + if (_currentAreaExplored) + Log.Debug("Progression: skipping exploration (town={Town}, questPassThrough={Pass})", + node.IsTown, _questDriven && !isAtTarget); + } + + // If we're in town and a quest targets this town, talk to NPC + if (node.IsTown && HasQuestInThisArea(state)) + { + UpdateTalkingToNpc(state, actions); + return; + } + + switch (_phase) + { + case Phase.Exploring: + UpdateExploring(state, actions); + break; + case Phase.Looting: + UpdateLooting(state, actions); + break; + case Phase.NavigatingToChest: + UpdateNavigatingToChest(state, actions); + break; + case Phase.InteractingChest: + UpdateInteractingChest(state, actions); + break; + case Phase.NavigatingToTransition: + UpdateNavigatingToTransition(state, actions); + break; + case Phase.Interacting: + UpdateInteracting(state, actions); + break; + case Phase.TalkingToNpc: + UpdateTalkingToNpc(state, actions); + break; + } + } + + private void UpdateExploring(GameState state, ActionQueue actions) + { + // ── Check 1: Yield for elite combat ── + const float EliteEngagementRange = 800f; + foreach (var m in state.HostileMonsters) + { + if (m.Rarity >= MonsterRarity.Rare && m.DistanceToPlayer < EliteEngagementRange) + { + if (_nav.Mode != NavMode.Idle) + { + Log.Information("Progression: yielding for {Rarity} (dist={Dist:F0})", m.Rarity, m.DistanceToPlayer); + _nav.Stop(); + } + return; + } + } + + // ── Check 2: Quest chest interaction ── + foreach (var e in state.Entities) + { + if (e.Path is null || !e.IsTargetable) continue; + if (!e.Path.Contains("QuestChestBase", StringComparison.OrdinalIgnoreCase)) continue; + if (_interactedChests.Contains(e.Id)) continue; + + _chestTargetEntityId = e.Id; + _chestInteractStartMs = state.TimestampMs; + Log.Information("Progression: found quest chest {Id} (dist={Dist:F0})", e.Id, e.DistanceToPlayer); + + if (e.DistanceToPlayer < 150f) + { + _phase = Phase.InteractingChest; + _nav.Stop(); + } + else + { + _phase = Phase.NavigatingToChest; + _nav.NavigateToEntity(e.Id); + } + return; + } + + // ── Check 3: Loot pickup when safe ── + const float LootPickupRange = 600f; + if (state.Danger <= DangerLevel.Low && state.NearbyLoot.Count > 0) + { + EntitySnapshot? nearestLoot = null; + foreach (var item in state.NearbyLoot) + { + if (item.DistanceToPlayer > LootPickupRange) continue; + if (nearestLoot is null || item.DistanceToPlayer < nearestLoot.DistanceToPlayer) + nearestLoot = item; + } + if (nearestLoot is not null) + { + _lootTargetEntityId = nearestLoot.Id; + _lootStartMs = state.TimestampMs; + _phase = Phase.Looting; + _nav.NavigateToEntity(nearestLoot.Id); + Log.Debug("Progression: looting item {Id} (dist={Dist:F0})", nearestLoot.Id, nearestLoot.DistanceToPlayer); + return; + } + } + + // If the current area hasn't been fully explored yet, keep exploring before + // looking for the exit transition. This prevents the bot from entering a + // dead-end area (like Mud Burrow) and immediately leaving without clearing it. + if (!_currentAreaExplored) + { + if (_nav.IsExplorationComplete) + { + _currentAreaExplored = true; + Log.Information("Progression: area exploration complete, now looking for transition to {Target}", + _targetTransitionName); + } + else + { + if (_nav.Mode != NavMode.Exploring) + _nav.Explore(); + return; // Don't scan for transitions yet + } + } + + // Throttle entity scanning to once per second + var now = state.TimestampMs; + if (now - _lastTransitionCheckMs < 1000) return; + _lastTransitionCheckMs = now; + + // Find the nearest targetable matching transition that isn't blacklisted + EntitySnapshot? best = null; + foreach (var e in state.Entities) + { + if (e.Category != EntityCategory.AreaTransition) continue; + if (!e.IsTargetable) continue; + if (!string.Equals(e.TransitionName, _targetTransitionName, StringComparison.OrdinalIgnoreCase)) continue; + if (_blacklistedTransitions.Contains(e.Id)) continue; + + if (best is null || e.DistanceToPlayer < best.DistanceToPlayer) + best = e; + } + + if (best is not null) + { + _targetTransitionEntityId = best.Id; + Log.Information("Progression: found transition to {Target} (entity {Id}, dist={Dist:F0})", + _targetTransitionName, best.Id, best.DistanceToPlayer); + + if (best.DistanceToPlayer < 150f) + { + _phase = Phase.Interacting; + _interactStartMs = state.TimestampMs; + _nav.Stop(); + } + else + { + _phase = Phase.NavigatingToTransition; + _nav.NavigateToEntity(best.Id); + } + return; + } + + // No transition found yet — explore + if (_nav.Mode != NavMode.Exploring) + _nav.Explore(); + } + + private void UpdateLooting(GameState state, ActionQueue actions) + { + // Danger spike — abandon loot, resume exploring + if (state.Danger >= DangerLevel.Medium) + { + Log.Debug("Progression: danger spike, abandoning loot"); + _lootTargetEntityId = 0; + _phase = Phase.Exploring; + return; + } + + // Elite appeared — yield to combat (will hit elite check in UpdateExploring next tick) + const float EliteEngagementRange = 800f; + foreach (var m in state.HostileMonsters) + { + if (m.Rarity >= MonsterRarity.Rare && m.DistanceToPlayer < EliteEngagementRange) + { + Log.Debug("Progression: elite appeared while looting, yielding"); + _lootTargetEntityId = 0; + _phase = Phase.Exploring; + _nav.Stop(); + return; + } + } + + // Timeout — entity unreachable + if (state.TimestampMs - _lootStartMs > 5000) + { + Log.Debug("Progression: loot pickup timeout on {Id}", _lootTargetEntityId); + _lootTargetEntityId = 0; + _phase = Phase.Exploring; + return; + } + + // Find the target loot entity + EntitySnapshot? target = null; + foreach (var item in state.NearbyLoot) + { + if (item.Id == _lootTargetEntityId) { target = item; break; } + } + + if (target is null) + { + // Target gone (picked up or despawned) — chain to next nearby loot + const float LootPickupRange = 600f; + EntitySnapshot? next = null; + foreach (var item in state.NearbyLoot) + { + if (item.DistanceToPlayer > LootPickupRange) continue; + if (next is null || item.DistanceToPlayer < next.DistanceToPlayer) + next = item; + } + + if (next is not null) + { + _lootTargetEntityId = next.Id; + _lootStartMs = state.TimestampMs; + _nav.NavigateToEntity(next.Id); + Log.Debug("Progression: chaining to next loot {Id} (dist={Dist:F0})", next.Id, next.DistanceToPlayer); + } + else + { + _lootTargetEntityId = 0; + _phase = Phase.Exploring; + Log.Debug("Progression: no more loot, resuming exploration"); + } + return; + } + + // Close enough — click to pick up + if (target.DistanceToPlayer < 100f) + { + if (state.CameraMatrix.HasValue) + { + var screenPos = WorldToScreen.Project(target.Position, state.Player.Z, state.CameraMatrix.Value); + if (screenPos.HasValue) + actions.Submit(new ClickAction(SystemPriority.Navigation - 1, screenPos.Value, ClickType.Left)); + } + return; + } + + // Otherwise NavigateToEntity handles approach (already set when entering phase) + } + + private void UpdateNavigatingToChest(GameState state, ActionQueue actions) + { + foreach (var e in state.Entities) + { + if (e.Id != _chestTargetEntityId) continue; + + if (e.DistanceToPlayer < 150f) + { + _phase = Phase.InteractingChest; + _chestInteractStartMs = state.TimestampMs; + _nav.Stop(); + return; + } + + // Entity still exists, keep navigating + return; + } + + // Entity disappeared — give up on this chest + Log.Debug("Progression: quest chest entity lost, resuming exploration"); + _interactedChests.Add(_chestTargetEntityId); + _chestTargetEntityId = 0; + _phase = Phase.Exploring; + } + + private void UpdateInteractingChest(GameState state, ActionQueue actions) + { + foreach (var e in state.Entities) + { + if (e.Id != _chestTargetEntityId) continue; + + if (!e.IsTargetable) + { + // Chest opened — done + Log.Information("Progression: quest chest {Id} opened", e.Id); + _interactedChests.Add(_chestTargetEntityId); + _chestTargetEntityId = 0; + _phase = Phase.Exploring; + return; + } + + if (state.CameraMatrix.HasValue) + { + var screenPos = WorldToScreen.Project(e.Position, state.Player.Z, state.CameraMatrix.Value); + if (screenPos.HasValue) + { + actions.Submit(new ClickAction(SystemPriority.Navigation - 1, screenPos.Value, ClickType.Left)); + return; + } + } + + // Can't project — walk closer + if (e.DistanceToPlayer > 100f) + { + _phase = Phase.NavigatingToChest; + _nav.NavigateToEntity(e.Id); + return; + } + return; + } + + // Entity gone or timeout + if (state.TimestampMs - _chestInteractStartMs > 5000) + { + Log.Warning("Progression: quest chest interaction timeout on {Id}", _chestTargetEntityId); + _interactedChests.Add(_chestTargetEntityId); + _chestTargetEntityId = 0; + _phase = Phase.Exploring; + } + } + + private void UpdateNavigatingToTransition(GameState state, ActionQueue actions) + { + // Check if the entity is still visible and close enough + foreach (var e in state.Entities) + { + if (e.Id != _targetTransitionEntityId) continue; + + if (e.DistanceToPlayer < 150f) + { + _phase = Phase.Interacting; + _interactStartMs = state.TimestampMs; + _nav.Stop(); + return; + } + + // Entity still exists, keep navigating + return; + } + + // Entity disappeared from view — go back to exploring + Log.Debug("Progression: transition entity lost, returning to explore"); + _targetTransitionEntityId = 0; + _phase = Phase.Exploring; + _nav.Explore(); + } + + private void UpdateInteracting(GameState state, ActionQueue actions) + { + // Project entity to screen and click + foreach (var e in state.Entities) + { + if (e.Id != _targetTransitionEntityId) continue; + + if (state.CameraMatrix.HasValue) + { + var screenPos = WorldToScreen.Project(e.Position, state.Player.Z, state.CameraMatrix.Value); + if (screenPos.HasValue) + { + actions.Submit(new ClickAction(SystemPriority.Navigation - 1, screenPos.Value, ClickType.Left)); + return; + } + } + + // Can't project — walk closer + if (e.DistanceToPlayer > 100f) + { + _phase = Phase.NavigatingToTransition; + _nav.NavigateToEntity(e.Id); + return; + } + return; + } + + // Entity gone or interaction failed — blacklist this transition and try another + if (state.TimestampMs - _interactStartMs > 5000) + { + Log.Warning("Progression: interaction timeout on entity {Id}, blacklisting and trying next", + _targetTransitionEntityId); + _blacklistedTransitions.Add(_targetTransitionEntityId); + _targetTransitionEntityId = 0; + _phase = Phase.Exploring; + _nav.Explore(); + } + } + + private string? FindQuestTarget(GameState state) + { + var candidates = state.Quests + .Where(q => q.TargetAreas is { Count: > 0 }) + .SelectMany(q => q.TargetAreas!.Select(a => new { Quest = q, Area = a })) + .Where(x => x.Area.Id is not null + && !string.Equals(x.Area.Id, _currentAreaId, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (candidates.Count == 0) return null; + + // Prefer tracked quests, then lowest act, then quests with shortest path + var best = candidates + .OrderByDescending(x => x.Quest.IsTracked) + .ThenBy(x => x.Area.Act) + .ThenBy(x => x.Quest.PathToTarget?.Count ?? 999) + .First(); + + return best.Area.Id; + } + + private bool HasQuestInThisArea(GameState state) + { + return state.Quests.Any(q => q.TargetAreas?.Any(a => + string.Equals(a.Id, _currentAreaId, StringComparison.OrdinalIgnoreCase)) == true); + } + + private void UpdateTalkingToNpc(GameState state, ActionQueue actions) + { + // Find nearest targetable NPC + EntitySnapshot? npc = null; + foreach (var e in state.Entities) + { + if (e.Category != EntityCategory.Npc || !e.IsTargetable) continue; + if (npc is null || e.DistanceToPlayer < npc.DistanceToPlayer) + npc = e; + } + + if (npc is null) return; + + if (npc.DistanceToPlayer < 150f) + { + if (state.CameraMatrix.HasValue) + { + var screenPos = WorldToScreen.Project(npc.Position, state.Player.Z, state.CameraMatrix.Value); + if (screenPos.HasValue) + actions.Submit(new ClickAction(Priority, screenPos.Value, ClickType.Left)); + } + } + else + { + _nav.NavigateToEntity(npc.Id); + } + } +} diff --git a/src/Nexus.Systems/BotTick.cs b/src/Nexus.Systems/BotTick.cs new file mode 100644 index 0000000..cc58374 --- /dev/null +++ b/src/Nexus.Systems/BotTick.cs @@ -0,0 +1,46 @@ +using Nexus.Core; +using Nexus.Data; +using Nexus.Pathfinding; + +namespace Nexus.Systems; + +public static class BotTick +{ + public static List Run( + GameState state, + List systems, + ActionQueue actionQueue, + MovementBlender movementBlender, + NavigationController nav, + BotConfig config) + { + GameStateEnricher.Enrich(state); + + actionQueue.Clear(); + movementBlender.Clear(); + nav.Update(state); + + foreach (var sys in systems) + if (sys.IsEnabled) + sys.Update(state, actionQueue, movementBlender); + + // Wall repulsion — push away from nearby walls to prevent getting stuck + if (state.Terrain is { } terrain && state.Player.HasPosition) + { + var wallPush = TerrainQuery.ComputeWallRepulsion(terrain, state.Player.Position, config.WorldToGrid); + if (wallPush.LengthSquared() > 0.0001f) + movementBlender.Submit(new MovementIntent(2, wallPush * 0.6f, 0.3f, "WallPush")); + } + + if (nav.DesiredDirection.HasValue) + movementBlender.Submit(new MovementIntent(3, nav.DesiredDirection.Value, 0f, "Navigation")); + + movementBlender.Resolve(state.Terrain, state.Player.Position, config.WorldToGrid); + var resolved = actionQueue.Resolve(); + + if (movementBlender.IsUrgentFlee) + resolved.RemoveAll(a => a is CastAction); + + return resolved; + } +} diff --git a/src/Nexus.Systems/CombatSystem.cs b/src/Nexus.Systems/CombatSystem.cs index 0309724..eb3cfa4 100644 --- a/src/Nexus.Systems/CombatSystem.cs +++ b/src/Nexus.Systems/CombatSystem.cs @@ -76,7 +76,7 @@ public class CombatSystem : ISystem _heldSlots.Clear(); } - public void Update(GameState state, ActionQueue actions) + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { if (state.CameraMatrix is not { } camera) return; @@ -92,16 +92,19 @@ public class CombatSystem : ISystem _lastAreaHash = state.AreaHash; } - // Global cooldown: don't cast if we recently cast any skill - if (now - _lastCastGlobal < _globalCooldownMs) + // Always submit orbit/herd intent when enemies are nearby — provides continuous + // circular movement that blends with other systems. Override is stronger during GCD + // (no cast competing) and weaker while casting (cast targeting takes priority). + var inGcd = now - _lastCastGlobal < _globalCooldownMs; + if (_kiteEnabled && state.NearestEnemies.Count > 0) { - // Orbit-herd during cooldown window (after cast animation delay) - if (_kiteEnabled && now - _lastCastGlobal >= _kiteDelayMs - && state.NearestEnemies.Count > 0) - { - TryHerd(state, actions); - } + var herdOverride = inGcd ? 0.4f : 0.2f; + TryHerd(state, movement, herdOverride); + } + // Global cooldown: don't cast if we recently cast any skill + if (inGcd) + { // Still need to handle MaintainPressed releases UpdateHeldKeys(state, camera, playerZ, actions); return; @@ -205,7 +208,7 @@ public class CombatSystem : ISystem /// Orbit-herding: move perpendicular to enemy centroid so scattered mobs converge /// into a tight cluster for AOE. Maintains ideal distance via radial bias. /// - private void TryHerd(GameState state, ActionQueue actions) + private void TryHerd(GameState state, MovementBlender movement, float overrideFactor) { var playerPos = state.Player.Position; @@ -241,26 +244,8 @@ public class CombatSystem : ISystem var dir = Vector2.Normalize(perp + centroidDir * radialBias); - // Validate against terrain — flip orbit direction on wall hit - if (state.Terrain is { } terrain) - { - var validated = TerrainQuery.FindWalkableDirection(terrain, playerPos, dir, _worldToGrid); - - // If terrain forced a significantly different direction, flip orbit - if (Vector2.Dot(validated, dir) < 0.5f) - { - _orbitSign *= -1; - perp = new Vector2(-centroidDir.Y, centroidDir.X) * _orbitSign; - dir = Vector2.Normalize(perp + centroidDir * radialBias); - dir = TerrainQuery.FindWalkableDirection(terrain, playerPos, dir, _worldToGrid); - } - else - { - dir = validated; - } - } - - actions.Submit(new MoveAction(SystemPriority.Combat, dir)); + // Layer 4: orbit/herd with variable override (stronger during GCD, weaker while casting) + movement.Submit(new MovementIntent(4, dir, overrideFactor, "Herd")); } private void UpdateHeldKeys(GameState state, Matrix4x4 camera, float playerZ, ActionQueue actions) diff --git a/src/Nexus.Systems/LootSystem.cs b/src/Nexus.Systems/LootSystem.cs index 0d34b87..ffa67f4 100644 --- a/src/Nexus.Systems/LootSystem.cs +++ b/src/Nexus.Systems/LootSystem.cs @@ -8,7 +8,7 @@ public class LootSystem : ISystem public string Name => "Loot"; public bool IsEnabled { get; set; } = false; - public void Update(GameState state, ActionQueue actions) + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { // STUB: loot detection and pickup logic } diff --git a/src/Nexus.Systems/MovementSystem.cs b/src/Nexus.Systems/MovementSystem.cs index bd46248..6ffeb4c 100644 --- a/src/Nexus.Systems/MovementSystem.cs +++ b/src/Nexus.Systems/MovementSystem.cs @@ -4,8 +4,10 @@ using Nexus.Core; namespace Nexus.Systems; /// -/// Force-based avoidance: applies inverse-square repulsion from hostile monsters -/// within safe distance. Emits a MoveAction with the escape direction. +/// Proximity-aware positioning: when enemies are within safe distance, applies a blend of +/// radial push (away from centroid) and tangential orbit (perpendicular to centroid). +/// The tangential component makes the bot circle around enemies instead of running backward. +/// Closer enemies produce stronger push-out, but always with orbit mixed in. /// public class MovementSystem : ISystem { @@ -14,41 +16,74 @@ public class MovementSystem : ISystem public bool IsEnabled { get; set; } = true; public float SafeDistance { get; set; } = 400f; - public float RepulsionWeight { get; set; } = 1.5f; + public float RepulsionWeight { get; set; } = 0.5f; /// World-to-grid conversion factor for terrain queries. public float WorldToGrid { get; set; } = 23f / 250f; - public void Update(GameState state, ActionQueue actions) + /// Minimum distance before radial push kicks in hard. + public float MinComfortDistance { get; set; } = 150f; + + private int _orbitSign = 1; + + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { if (!state.Player.HasPosition) return; if (state.HostileMonsters.Count == 0) return; var playerPos = state.Player.Position; - var repulsion = Vector2.Zero; + + // Compute weighted centroid and closest distance of nearby hostiles + var centroid = Vector2.Zero; + var count = 0; + var closestDist = float.MaxValue; foreach (var monster in state.HostileMonsters) { if (!monster.IsAlive) continue; if (monster.DistanceToPlayer > SafeDistance) continue; - var delta = playerPos - monster.Position; - var distSq = delta.LengthSquared(); - if (distSq < 1f) distSq = 1f; - - // Inverse-square repulsion: stronger when closer - var force = delta / distSq * RepulsionWeight; - repulsion += force; + centroid += monster.Position; + count++; + if (monster.DistanceToPlayer < closestDist) + closestDist = monster.DistanceToPlayer; } - if (repulsion.LengthSquared() < 0.0001f) return; + if (count == 0) return; + centroid /= count; - var direction = Vector2.Normalize(repulsion); + var toCentroid = centroid - playerPos; + var dist = toCentroid.Length(); + if (dist < 1f) return; - // Validate repulsion direction against terrain — avoid walking into walls - if (state.Terrain is { } terrain) - direction = TerrainQuery.FindWalkableDirection(terrain, state.Player.Position, direction, WorldToGrid); + var centroidDir = toCentroid / dist; - actions.Enqueue(new MoveAction(Priority, direction)); + // Tangential component — perpendicular to centroid direction (orbit) + var tangent = new Vector2(-centroidDir.Y, centroidDir.X) * _orbitSign; + + // Radial component — push away from centroid, strength based on proximity + // Close < MinComfort: strong push out + // MinComfort..SafeDistance*0.5: gentle push out + // SafeDistance*0.7+: pull inward to maintain engagement instead of drifting away + float radialStrength; + if (closestDist < MinComfortDistance) + radialStrength = -0.6f; // too close — push outward + else if (closestDist < SafeDistance * 0.5f) + radialStrength = -0.3f; // somewhat close — moderate push outward + else if (closestDist > SafeDistance * 0.7f) + radialStrength = 0.4f; // at edge — pull inward to maintain engagement + else + radialStrength = 0f; // sweet spot — pure orbit + + // Blend: mostly tangent (circling) with radial bias + var result = tangent + centroidDir * radialStrength; + + if (result.LengthSquared() < 0.0001f) return; + + // Override: attenuate navigation (layer 3) when actively orbiting enemies. + // Without this, navigation at full weight pulls the bot past enemies. + float orbitOverride = closestDist < SafeDistance * 0.7f ? 0.8f : 0.5f; + + movement.Submit(new MovementIntent(2, Vector2.Normalize(result) * RepulsionWeight, orbitOverride, "Orbit")); } } diff --git a/src/Nexus.Systems/NavigationSystem.cs b/src/Nexus.Systems/NavigationSystem.cs index f611070..bded23e 100644 --- a/src/Nexus.Systems/NavigationSystem.cs +++ b/src/Nexus.Systems/NavigationSystem.cs @@ -21,9 +21,9 @@ public class NavigationSystem : ISystem ///
public Vector2? ExternalDirection { get; set; } - public void Update(GameState state, ActionQueue actions) + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { if (ExternalDirection.HasValue) - actions.Submit(new MoveAction(Priority, ExternalDirection.Value)); + movement.Submit(new MovementIntent(3, ExternalDirection.Value, 0f, "Navigation")); } } diff --git a/src/Nexus.Systems/Nexus.Systems.csproj b/src/Nexus.Systems/Nexus.Systems.csproj index 60b2b65..0980613 100644 --- a/src/Nexus.Systems/Nexus.Systems.csproj +++ b/src/Nexus.Systems/Nexus.Systems.csproj @@ -9,5 +9,7 @@ + + diff --git a/src/Nexus.Systems/ResourceSystem.cs b/src/Nexus.Systems/ResourceSystem.cs index 3205681..d00c116 100644 --- a/src/Nexus.Systems/ResourceSystem.cs +++ b/src/Nexus.Systems/ResourceSystem.cs @@ -29,7 +29,7 @@ public class ResourceSystem : ISystem _lastManaFlaskMs = 0; } - public void Update(GameState state, ActionQueue actions) + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { var player = state.Player; if (player.LifeTotal == 0) return; diff --git a/src/Nexus.Systems/SystemFactory.cs b/src/Nexus.Systems/SystemFactory.cs new file mode 100644 index 0000000..27aa401 --- /dev/null +++ b/src/Nexus.Systems/SystemFactory.cs @@ -0,0 +1,35 @@ +using Nexus.Core; +using Nexus.Data; +using Nexus.Pathfinding; + +namespace Nexus.Systems; + +public static class SystemFactory +{ + public static List CreateSystems(BotConfig config, NavigationController nav, + bool includeAreaProgression = false) + { + var systems = new List(); + + if (includeAreaProgression) + systems.Add(new AreaProgressionSystem(config, nav, AreaGraph.Load())); + + systems.Add(new ThreatSystem { WorldToGrid = config.WorldToGrid }); + systems.Add(new MovementSystem + { + SafeDistance = config.SafeDistance, + RepulsionWeight = config.RepulsionWeight, + WorldToGrid = config.WorldToGrid, + }); + systems.Add(new NavigationSystem + { + WorldToGrid = config.WorldToGrid, + WaypointReachedDistance = config.WaypointReachedDistance, + }); + systems.Add(new CombatSystem(config)); + systems.Add(new ResourceSystem(config)); + systems.Add(new LootSystem()); + + return systems; + } +} diff --git a/src/Nexus.Systems/ThreatSystem.cs b/src/Nexus.Systems/ThreatSystem.cs index bd44615..7810879 100644 --- a/src/Nexus.Systems/ThreatSystem.cs +++ b/src/Nexus.Systems/ThreatSystem.cs @@ -5,10 +5,9 @@ using Serilog; namespace Nexus.Systems; /// -/// Emergency threat response. Runs first (priority 50). -/// On Critical danger: urgent flee (blocks casting via priority ≤ 10). -/// On High danger: flee toward safety but allow casting. -/// Medium and below: no action (MovementSystem handles soft avoidance). +/// Emergency-only threat response. Runs first (priority 50). +/// Only fires on Critical danger (low HP or overwhelming threat score). +/// Normal combat (High/Medium) is handled by MovementSystem orbiting + CombatSystem herding. /// public class ThreatSystem : ISystem { @@ -16,18 +15,15 @@ public class ThreatSystem : ISystem public string Name => "Threat"; public bool IsEnabled { get; set; } = true; - /// Priority ≤ 10 blocks casting in ActionQueue.Resolve — pure flee. - private const int UrgentFleePriority = 5; - - /// If closest enemy is within this range, escalate to urgent flee. - public float PointBlankRange { get; set; } = 150f; + /// If closest enemy is within this range AND danger is Critical, escalate to urgent flee. + public float PointBlankRange { get; set; } = 120f; /// World-to-grid conversion factor for terrain queries. public float WorldToGrid { get; set; } = 23f / 250f; private DangerLevel _prevDanger = DangerLevel.Safe; - public void Update(GameState state, ActionQueue actions) + public void Update(GameState state, ActionQueue actions, MovementBlender movement) { if (!state.Player.HasPosition) return; @@ -45,32 +41,28 @@ public class ThreatSystem : ISystem _prevDanger = danger; } - if (danger <= DangerLevel.Medium) return; + // Only respond to Critical danger — High is normal combat, handled by orbit/herd + if (danger != DangerLevel.Critical) return; if (threats.TotalHostiles == 0) return; // Compute flee direction: away from threat centroid var fleeDir = state.Player.Position - threats.ThreatCentroid; if (fleeDir.LengthSquared() < 0.0001f) - fleeDir = Vector2.UnitY; // fallback if at centroid + fleeDir = Vector2.UnitY; fleeDir = Vector2.Normalize(fleeDir); - // Validate flee direction against terrain — avoid walking into walls - if (state.Terrain is { } terrain) - fleeDir = TerrainQuery.FindWalkableDirection(terrain, state.Player.Position, fleeDir, WorldToGrid); - - // Point-blank override: if closest enemy is very close, escalate to urgent var isPointBlank = threats.ClosestDistance < PointBlankRange; - if (danger == DangerLevel.Critical || isPointBlank) + if (isPointBlank) { - // Urgent flee — blocks casting (priority ≤ 10) - actions.Submit(new MoveAction(UrgentFleePriority, fleeDir)); + // Layer 0: total override — pure flee, blocks casting + movement.Submit(new MovementIntent(0, fleeDir, 1.0f, "Threat")); } - else // High + else { - // Flee but allow casting alongside - actions.Submit(new MoveAction(Priority, fleeDir)); + // Layer 1: strong flee but allow some nav/orbit bleed-through + movement.Submit(new MovementIntent(1, fleeDir, 0.6f, "Threat")); } } } From 703cfbfdee0d72766a27e097d4d5a222ddf483c9 Mon Sep 17 00:00:00 2001 From: Boki Date: Sat, 7 Mar 2026 12:27:25 -0500 Subject: [PATCH 3/5] threat better --- imgui.ini | 6 +- src/Nexus.Core/GameState.cs | 3 +- src/Nexus.Core/MovementBlender.cs | 38 +- src/Nexus.Core/MovementKeyTracker.cs | 33 +- src/Nexus.Core/ThreatAssessment.cs | 51 +++ src/Nexus.Core/ThreatMap.cs | 2 +- src/Nexus.Data/GameStateEnricher.cs | 145 +----- src/Nexus.Simulator/Config/SimConfig.cs | 6 +- src/Nexus.Simulator/Program.cs | 23 +- src/Nexus.Simulator/Rendering/DebugPanel.cs | 41 +- src/Nexus.Simulator/Rendering/SimRenderer.cs | 3 +- src/Nexus.Systems/ThreatSystem.cs | 458 +++++++++++++++++-- 12 files changed, 581 insertions(+), 228 deletions(-) create mode 100644 src/Nexus.Core/ThreatAssessment.cs diff --git a/imgui.ini b/imgui.ini index 039b318..ef2059b 100644 --- a/imgui.ini +++ b/imgui.ini @@ -4,12 +4,12 @@ Size=400,400 Collapsed=0 [Window][Simulator Controls] -Pos=60,60 -Size=219,425 +Pos=29,51 +Size=432,649 Collapsed=0 [Window][Simulator] -Pos=341,232 +Pos=499,177 Size=1200,681 Collapsed=0 diff --git a/src/Nexus.Core/GameState.cs b/src/Nexus.Core/GameState.cs index 7baf4c7..e7125c0 100644 --- a/src/Nexus.Core/GameState.cs +++ b/src/Nexus.Core/GameState.cs @@ -27,8 +27,9 @@ public class GameState /// In-progress quests from the quest linked list with target areas and paths. public IReadOnlyList Quests { get; set; } = []; - // Derived (computed once per tick by GameStateEnricher) + // Derived (computed once per tick by GameStateEnricher / ThreatSystem) public ThreatMap Threats { get; set; } = new(); + public ThreatAssessment ThreatAssessment { get; set; } = new(); public IReadOnlyList NearestEnemies { get; set; } = []; public IReadOnlyList GroundEffects { get; set; } = []; } diff --git a/src/Nexus.Core/MovementBlender.cs b/src/Nexus.Core/MovementBlender.cs index 0b25de0..8fa46ed 100644 --- a/src/Nexus.Core/MovementBlender.cs +++ b/src/Nexus.Core/MovementBlender.cs @@ -28,15 +28,14 @@ public sealed class MovementBlender // Snap decision based on INTENT change (pre-terrain), not terrain output — prevents // terrain probe noise from bypassing the EMA via the snap threshold. private Vector2? _smoothedDirection; - private Vector2? _lastIntentDir; // pre-terrain direction from previous frame - private const float SmoothingAlpha = 0.12f; // 12% new, 88% previous + private const float SmoothingAlpha = 0.20f; // 20% new, 80% previous - // Terrain validation cache — prevents re-probing within the same grid cell, + // Terrain validation cache — prevents re-probing within a small radius, // breaking the position↔direction feedback loop that causes zigzag oscillation private Vector2 _cachedTerrainInputDir; private Vector2 _cachedTerrainResult; - private int _cachedTerrainGridX = int.MinValue; - private int _cachedTerrainGridY = int.MinValue; + private Vector2 _cachedTerrainPos; + private const float TerrainCacheRadius = 20f; // don't re-probe within 20 world units public Vector2? Direction { get; private set; } public Vector2? RawDirection { get; private set; } @@ -130,19 +129,16 @@ public sealed class MovementBlender // Normalize the blended result var rawDir = Vector2.Normalize(result); - var intentDir = rawDir; // save pre-terrain direction for snap decision // Terrain validation with grid-cell caching. // Re-probe only when the raw direction changes (>~14°) or the player enters a new grid cell. // This prevents the feedback loop: direction jitter → zigzag movement → crosses cell boundary → more jitter. if (terrain is not null) { - var gx = (int)(playerPos.X * worldToGrid); - var gy = (int)(playerPos.Y * worldToGrid); var dirSimilar = Vector2.Dot(rawDir, _cachedTerrainInputDir) > 0.97f; - var sameCell = gx == _cachedTerrainGridX && gy == _cachedTerrainGridY; + var nearbyPos = Vector2.DistanceSquared(playerPos, _cachedTerrainPos) < TerrainCacheRadius * TerrainCacheRadius; - if (dirSimilar && sameCell) + if (dirSimilar && nearbyPos) { rawDir = _cachedTerrainResult; } @@ -152,28 +148,23 @@ public sealed class MovementBlender rawDir = TerrainQuery.FindWalkableDirection(terrain, playerPos, rawDir, worldToGrid); _cachedTerrainInputDir = preTerrainDir; _cachedTerrainResult = rawDir; - _cachedTerrainGridX = gx; - _cachedTerrainGridY = gy; + _cachedTerrainPos = playerPos; } } RawDirection = rawDir; - // EMA smoothing. Snap decision based on whether the INTENT (pre-terrain) changed, - // not the terrain output. This prevents terrain probe noise (which can produce 90°+ swings) - // from bypassing the EMA via the snap threshold. + // EMA smoothing. Only snap (bypass smoothing) on urgent flee (L0), + // which needs instant response. All other direction changes (orbit flips, + // terrain jitter, waypoint changes) get smoothed to prevent oscillation. if (_smoothedDirection.HasValue) { - var intentChanged = _lastIntentDir.HasValue && - Vector2.Dot(_lastIntentDir.Value, intentDir) < 0f; - - if (intentChanged) + if (IsUrgentFlee) { - // Genuine intent reversal (flee, new waypoint) — snap immediately + // Emergency flee — snap immediately, no smoothing } else { - // Intent is stable — all direction change is terrain noise, always smooth var smoothed = Vector2.Lerp(_smoothedDirection.Value, rawDir, SmoothingAlpha); if (smoothed.LengthSquared() > 0.0001f) rawDir = Vector2.Normalize(smoothed); @@ -181,7 +172,6 @@ public sealed class MovementBlender } _smoothedDirection = rawDir; - _lastIntentDir = intentDir; Direction = rawDir; } @@ -199,9 +189,7 @@ public sealed class MovementBlender _stuckFrames = 0; _lastResolvePos = Vector2.Zero; _smoothedDirection = null; - _lastIntentDir = null; - _cachedTerrainGridX = int.MinValue; - _cachedTerrainGridY = int.MinValue; + _cachedTerrainPos = new Vector2(float.MinValue, float.MinValue); } /// diff --git a/src/Nexus.Core/MovementKeyTracker.cs b/src/Nexus.Core/MovementKeyTracker.cs index 3f56c64..59204d1 100644 --- a/src/Nexus.Core/MovementKeyTracker.cs +++ b/src/Nexus.Core/MovementKeyTracker.cs @@ -14,6 +14,8 @@ public sealed class MovementKeyTracker private bool _wHeld, _aHeld, _sHeld, _dHeld; private long _wDownAt, _aDownAt, _sDownAt, _dDownAt; private int _wMinHold, _aMinHold, _sMinHold, _dMinHold; + private long _wUpAt, _aUpAt, _sUpAt, _dUpAt; + private int _wRepress, _aRepress, _sRepress, _dRepress; private Vector2? _lastPlayerPos; private static readonly Random Rng = new(); @@ -55,10 +57,10 @@ public sealed class MovementKeyTracker } var now = Environment.TickCount64; - SetKey(input, ScanCodes.W, ref _wHeld, ref _wDownAt, ref _wMinHold, wantW, now, _lastPlayerPos); - SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, wantA, now, _lastPlayerPos); - SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, wantS, now, _lastPlayerPos); - SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, wantD, now, _lastPlayerPos); + SetKey(input, ScanCodes.W, ref _wHeld, ref _wDownAt, ref _wMinHold, ref _wUpAt, ref _wRepress, wantW, now, _lastPlayerPos); + SetKey(input, ScanCodes.A, ref _aHeld, ref _aDownAt, ref _aMinHold, ref _aUpAt, ref _aRepress, wantA, now, _lastPlayerPos); + SetKey(input, ScanCodes.S, ref _sHeld, ref _sDownAt, ref _sMinHold, ref _sUpAt, ref _sRepress, wantS, now, _lastPlayerPos); + SetKey(input, ScanCodes.D, ref _dHeld, ref _dDownAt, ref _dMinHold, ref _dUpAt, ref _dRepress, wantD, now, _lastPlayerPos); } /// @@ -78,10 +80,14 @@ public sealed class MovementKeyTracker }; private static void SetKey(IInputController input, ushort scanCode, - ref bool held, ref long downAt, ref int minHold, bool want, long now, Vector2? pos) + ref bool held, ref long downAt, ref int minHold, + ref long upAt, ref int repressDelay, bool want, long now, Vector2? pos) { if (want && !held) { + // Enforce re-press cooldown after release + if (now - upAt < repressDelay) return; + input.KeyDown(scanCode); held = true; downAt = now; @@ -96,8 +102,11 @@ public sealed class MovementKeyTracker { var elapsed = now - downAt; if (elapsed < minHold) return; // enforce minimum hold + input.KeyUp(scanCode); held = false; + upAt = now; + repressDelay = RepressMs(); if (pos.HasValue) Log.Information("[WASD] {Key} UP (held={Elapsed}ms, min={MinHold}ms) pos=({X:F0},{Y:F0})", KeyName(scanCode), elapsed, minHold, pos.Value.X, pos.Value.Y); @@ -119,4 +128,18 @@ public sealed class MovementKeyTracker var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s); return Math.Clamp((int)Math.Round(55.0 + g * 6.0), 44, 76); } + + /// Gaussian re-press cooldown peaked at 40ms, range [25, 65]. + private static int RepressMs() + { + double u, v, s; + do + { + u = Rng.NextDouble() * 2.0 - 1.0; + v = Rng.NextDouble() * 2.0 - 1.0; + s = u * u + v * v; + } while (s >= 1.0 || s == 0.0); + var g = u * Math.Sqrt(-2.0 * Math.Log(s) / s); + return Math.Clamp((int)Math.Round(40.0 + g * 8.0), 25, 65); + } } diff --git a/src/Nexus.Core/ThreatAssessment.cs b/src/Nexus.Core/ThreatAssessment.cs new file mode 100644 index 0000000..f7a195b --- /dev/null +++ b/src/Nexus.Core/ThreatAssessment.cs @@ -0,0 +1,51 @@ +using System.Numerics; + +namespace Nexus.Core; + +public enum ThreatCategory +{ + Ignore, // score ≤ 2 — not worth reacting to + Monitor, // score 2–6 — track but don't change behavior + Engage, // score 6–15 — fight, stay mobile + Flee, // score 15–25 — kite aggressively + Emergency, // score > 25 OR player HP critical — run, pop flasks +} + +public class ThreatEntry +{ + public uint EntityId { get; init; } + public Vector2 Position { get; set; } + public float DistanceToPlayer { get; set; } + public float ThreatScore { get; set; } + public float PerceivedDanger { get; set; } // normalized 0..1 + public ThreatCategory Category { get; set; } + public bool HasLineOfSight { get; set; } + public MonsterRarity Rarity { get; init; } + public float HpPercent { get; set; } + public bool IsAlive { get; set; } +} + +public class ThreatAssessment +{ + public List Entries { get; set; } = []; + + // Precomputed aggregates — consumed by steering, combat, state machine + public float ZoneThreatLevel { get; set; } // sum of all scores + public ThreatEntry? PrimaryTarget { get; set; } // best kill target + public ThreatEntry? MostDangerous { get; set; } // highest threat score + public Vector2 ThreatCentroid { get; set; } // score-weighted center + public Vector2 SafestDirection { get; set; } // away from centroid + public bool AnyEmergency { get; set; } + public bool ShouldFlee { get; set; } // zone threat > flee threshold + public bool AreaClear { get; set; } // no Monitor+ threats remain + public float ClosestDistance { get; set; } + + /// Continuous 0..1 flee weight for steering blend. + public float FleeWeight { get; set; } + + // Convenience — backward compatibility + public int CloseRange { get; set; } // < 300 + public int MidRange { get; set; } // 300–600 + public int FarRange { get; set; } // 600–1200 + public bool HasRareOrUnique { get; set; } +} diff --git a/src/Nexus.Core/ThreatMap.cs b/src/Nexus.Core/ThreatMap.cs index bfeac13..4d65d9c 100644 --- a/src/Nexus.Core/ThreatMap.cs +++ b/src/Nexus.Core/ThreatMap.cs @@ -8,7 +8,7 @@ public class ThreatMap public int CloseRange { get; init; } // < 300 units public int MidRange { get; init; } // 300–600 public int FarRange { get; init; } // 600–1200 - public float ClosestDistance { get; init; } = float.MaxValue; + public float ClosestDistance { get; init; } public Vector2 ThreatCentroid { get; init; } public bool HasRareOrUnique { get; init; } } diff --git a/src/Nexus.Data/GameStateEnricher.cs b/src/Nexus.Data/GameStateEnricher.cs index 545dc47..2d68fe9 100644 --- a/src/Nexus.Data/GameStateEnricher.cs +++ b/src/Nexus.Data/GameStateEnricher.cs @@ -4,16 +4,14 @@ using Nexus.Core; namespace Nexus.Data; /// -/// Computes all derived fields on GameState once per tick. -/// Static methods, no allocations beyond the sorted list. +/// Computes derived fields on GameState once per tick. +/// Threat scoring is now handled by ThreatSystem (runs as ISystem). /// public static class GameStateEnricher { public static void Enrich(GameState state) { state.NearestEnemies = ComputeNearestEnemies(state.HostileMonsters); - state.Threats = ComputeThreatMap(state.HostileMonsters); - state.Danger = ComputeDangerLevel(state); state.GroundEffects = []; // stub until memory reads ground effects } @@ -25,143 +23,4 @@ public static class GameStateEnricher sorted.Sort((a, b) => a.DistanceToPlayer.CompareTo(b.DistanceToPlayer)); return sorted; } - - private static ThreatMap ComputeThreatMap(IReadOnlyList hostiles) - { - if (hostiles.Count == 0) return new ThreatMap(); - - int close = 0, mid = 0, far = 0; - float closest = float.MaxValue; - var weightedSum = Vector2.Zero; - bool hasRareOrUnique = false; - - foreach (var m in hostiles) - { - var d = m.DistanceToPlayer; - if (d < closest) closest = d; - - if (d < 300f) close++; - else if (d < 600f) mid++; - else if (d < 1200f) far++; - - weightedSum += m.Position; - - if (m.ThreatLevel is MonsterThreatLevel.Rare or MonsterThreatLevel.Unique) - hasRareOrUnique = true; - } - - return new ThreatMap - { - TotalHostiles = hostiles.Count, - CloseRange = close, - MidRange = mid, - FarRange = far, - ClosestDistance = closest, - ThreatCentroid = weightedSum / hostiles.Count, - HasRareOrUnique = hasRareOrUnique, - }; - } - - /// - /// Computes danger using effective HP (life + energy shield) and a weighted threat score. - /// Close enemies count more, rares/uniques escalate significantly. - /// Hysteresis: de-escalation requires a larger margin than escalation to prevent oscillation. - /// - private static DangerLevel _previousDanger = DangerLevel.Safe; - private static float _smoothedThreatScore; - private static long _lastEscalationMs; - - private static DangerLevel ComputeDangerLevel(GameState state) - { - // Effective HP = life + ES combined - var effectiveHp = state.Player.LifeCurrent + state.Player.EsCurrent; - var effectiveMax = state.Player.LifeTotal + state.Player.EsTotal; - var effectivePercent = effectiveMax > 0 ? (float)effectiveHp / effectiveMax * 100f : 0f; - - // Pure life check — if ES is gone and life is low, it's critical (no hysteresis) - if (state.Player.LifePercent < 25f) - { - _previousDanger = DangerLevel.Critical; - return DangerLevel.Critical; - } - if (effectivePercent < 35f) - { - _previousDanger = DangerLevel.Critical; - return DangerLevel.Critical; - } - if (effectivePercent < 50f) - { - var hpLevel = DangerLevel.High; - if (hpLevel < _previousDanger) - hpLevel = _previousDanger; // don't de-escalate from HP alone - _previousDanger = hpLevel; - return hpLevel; - } - - // Weighted threat score: smooth distance falloff × rarity multiplier - var threatScore = 0f; - foreach (var m in state.HostileMonsters) - { - var d = m.DistanceToPlayer; - if (d > 800f) continue; - - // Smooth distance weight: linear falloff from 3.0 at d=0 to 0.5 at d=800 - var distWeight = 3f - 2.5f * (d / 800f); - - // Rarity multiplier - var rarityMul = m.Rarity switch - { - MonsterRarity.Unique => 5f, - MonsterRarity.Rare => 3f, - MonsterRarity.Magic => 1.5f, - _ => 1f, - }; - - threatScore += distWeight * rarityMul; - } - - // EMA smoothing — prevents single-frame score spikes from causing oscillation. - // Snap upward (escalation is instant), smooth downward (de-escalation is gradual). - const float deescalationAlpha = 0.08f; - if (threatScore >= _smoothedThreatScore) - _smoothedThreatScore = threatScore; // snap up — react instantly to new threats - else - _smoothedThreatScore += (threatScore - _smoothedThreatScore) * deescalationAlpha; - threatScore = _smoothedThreatScore; - - // Escalation thresholds - var level = DangerLevel.Safe; - if (threatScore >= 15f) level = DangerLevel.Critical; - else if (threatScore >= 8f) level = DangerLevel.High; - else if (threatScore >= 4f) level = DangerLevel.Medium; - else if (threatScore > 0f) level = DangerLevel.Low; - - // Hysteresis: minimum hold time + score margins prevent oscillation - var now = Environment.TickCount64; - if (level != _previousDanger) - { - // Hold any level for at least 1.5 seconds before allowing ANY transition - if (now - _lastEscalationMs < 1500) - { - level = _previousDanger; - } - else if (level < _previousDanger) - { - // Score-based hysteresis — only drop one level at a time - if (_previousDanger >= DangerLevel.Critical) - level = DangerLevel.High; - else if (_previousDanger >= DangerLevel.High) - level = DangerLevel.Medium; - else if (_previousDanger >= DangerLevel.Medium && threatScore >= 2f) - level = DangerLevel.Medium; - } - } - - // Track any transition - if (level != _previousDanger) - _lastEscalationMs = now; - - _previousDanger = level; - return level; - } } diff --git a/src/Nexus.Simulator/Config/SimConfig.cs b/src/Nexus.Simulator/Config/SimConfig.cs index e0c23e0..567f085 100644 --- a/src/Nexus.Simulator/Config/SimConfig.cs +++ b/src/Nexus.Simulator/Config/SimConfig.cs @@ -22,7 +22,7 @@ public class SimConfig public float EnemyAggroRange { get; set; } = 600f; public float EnemyMeleeAttackRange { get; set; } = 100f; public float EnemyMoveSpeedFactor { get; set; } = 0.75f; - public int EnemyBaseHealth { get; set; } = 200; + public int EnemyBaseHealth { get; set; } = 500; public int EnemyMeleeBaseDamage { get; set; } = 60; public float EnemyMeleeAttackCooldown { get; set; } = 1.2f; @@ -42,8 +42,8 @@ public class SimConfig public float EnemySpawnMinDist { get; set; } = 800f; public float EnemySpawnMaxDist { get; set; } = 2000f; public float EnemyCullDist { get; set; } = 3000f; - public int EnemyGroupMin { get; set; } = 3; - public int EnemyGroupMax { get; set; } = 7; + public int EnemyGroupMin { get; set; } = 7; + public int EnemyGroupMax { get; set; } = 18; public float EnemyGroupSpread { get; set; } = 120f; // Player skills diff --git a/src/Nexus.Simulator/Program.cs b/src/Nexus.Simulator/Program.cs index aecf079..891338d 100644 --- a/src/Nexus.Simulator/Program.cs +++ b/src/Nexus.Simulator/Program.cs @@ -18,6 +18,12 @@ Log.Logger = new LoggerConfiguration() .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); +// Parse --ticks N for headless auto-exit +int? maxTicks = null; +for (int i = 0; i < args.Length - 1; i++) + if (args[i] == "--ticks" && int.TryParse(args[i + 1], out var t)) + maxTicks = t; + Log.Information("Nexus Simulator starting..."); // ── Configuration ── @@ -75,6 +81,7 @@ var movementBlender = new MovementBlender(); var moveTracker = new MovementKeyTracker(); var botRunning = true; var lastStatusLogMs = 0L; +var botTickCount = 0; var botThread = new Thread(() => { @@ -91,6 +98,13 @@ var botThread = new Thread(() => var resolved = BotTick.Run(state, systems, actionQueue, movementBlender, nav, botConfig); ActionExecutor.Execute(resolved, input, moveTracker, movementBlender, state.Player.Position); + botTickCount++; + if (maxTicks.HasValue && botTickCount >= maxTicks.Value) + { + botRunning = false; + break; + } + // Periodic status log (every 2 seconds) var nowMs = Environment.TickCount64; if (nowMs - lastStatusLogMs >= 2000) @@ -110,12 +124,15 @@ var botThread = new Thread(() => })); if (actions.Length == 0) actions = "none"; + var ta = state.ThreatAssessment; Log.Information( - "Status: HP={HP}/{MaxHP} ES={ES}/{MaxES} Mana={MP}/{MaxMP} Danger={Danger} " + + "Status: HP={HP}/{MaxHP} ES={ES}/{MaxES} Mana={MP}/{MaxMP} " + + "Threat={Zone:F1}(fw={Fw:F2}) " + "Enemies={Total}({Melee}m/{Ranged}r) Nav={NavMode} Actions=[{Actions}] " + "Move=[{Blender}]", p.LifeCurrent, p.LifeTotal, p.EsCurrent, p.EsTotal, p.ManaCurrent, p.ManaTotal, - state.Danger, melee + ranged, melee, ranged, + ta.ZoneThreatLevel, ta.FleeWeight, + melee + ranged, melee, ranged, nav.Mode, actions, movementBlender.DiagnosticSummary()); } } @@ -172,7 +189,7 @@ window.Resized += () => var renderSw = System.Diagnostics.Stopwatch.StartNew(); var lastRenderMs = 0.0; -while (window.Exists) +while (window.Exists && botRunning) { var nowMs = renderSw.Elapsed.TotalMilliseconds; var deltaSeconds = (float)((nowMs - lastRenderMs) / 1000.0); diff --git a/src/Nexus.Simulator/Rendering/DebugPanel.cs b/src/Nexus.Simulator/Rendering/DebugPanel.cs index 9b67f9c..d7a5012 100644 --- a/src/Nexus.Simulator/Rendering/DebugPanel.cs +++ b/src/Nexus.Simulator/Rendering/DebugPanel.cs @@ -16,6 +16,15 @@ public class DebugPanel private int _spawnRarity; // 0=Normal, 1=Magic, 2=Rare, 3=Unique + // Cached threat display — updated at ~4Hz to prevent visual flicker + private long _lastThreatUpdateMs; + private string _cachedZoneLine = ""; + private string _cachedDangerLine = ""; + private string _cachedRangeLine = ""; + private string _cachedClosestLine = ""; + private string _cachedTopThreatLine = "Top Threat: None"; + private string _cachedKillTargetLine = "Kill Target: None"; + public DebugPanel(SimConfig config, SimWorld world, NavigationController nav, IReadOnlyList systems) { _config = config; @@ -108,14 +117,32 @@ public class DebugPanel } } - // Threat info - if (state is not null && ImGui.CollapsingHeader("Threat")) + // Threat info — cached at ~4Hz to prevent visual flicker + if (state is not null && ImGui.CollapsingHeader("Threat", ImGuiTreeNodeFlags.DefaultOpen)) { - ImGui.Text($"Danger: {state.Danger}"); - var threats = state.Threats; - ImGui.Text($"Close: {threats.CloseRange} Mid: {threats.MidRange} Far: {threats.FarRange}"); - ImGui.Text($"Closest: {threats.ClosestDistance:F0}"); - ImGui.Text($"Has Rare/Unique: {threats.HasRareOrUnique}"); + var nowMs = Environment.TickCount64; + if (nowMs - _lastThreatUpdateMs >= 250) + { + _lastThreatUpdateMs = nowMs; + var ta = state.ThreatAssessment; + _cachedZoneLine = $"Zone Threat: {ta.ZoneThreatLevel:F1} Flee Weight: {ta.FleeWeight:F2}"; + _cachedDangerLine = $"Danger: {state.Danger} Flee: {ta.ShouldFlee} Emergency: {ta.AnyEmergency}"; + _cachedRangeLine = $"Close: {ta.CloseRange} Mid: {ta.MidRange} Far: {ta.FarRange}"; + _cachedClosestLine = $"Closest: {ta.ClosestDistance:F0} Rare/Unique: {ta.HasRareOrUnique}"; + _cachedTopThreatLine = ta.MostDangerous is { } top + ? $"Top Threat: #{top.EntityId} ({top.Rarity}) score={top.ThreatScore:F1} cat={top.Category}" + : "Top Threat: None"; + _cachedKillTargetLine = ta.PrimaryTarget is { } pt + ? $"Kill Target: #{pt.EntityId} ({pt.Rarity}) HP={pt.HpPercent:P0} dist={pt.DistanceToPlayer:F0}" + : "Kill Target: None"; + } + + ImGui.Text(_cachedZoneLine); + ImGui.Text(_cachedDangerLine); + ImGui.Text(_cachedRangeLine); + ImGui.Text(_cachedClosestLine); + ImGui.Text(_cachedTopThreatLine); + ImGui.Text(_cachedKillTargetLine); } // Action queue diff --git a/src/Nexus.Simulator/Rendering/SimRenderer.cs b/src/Nexus.Simulator/Rendering/SimRenderer.cs index 79d3860..1b1a344 100644 --- a/src/Nexus.Simulator/Rendering/SimRenderer.cs +++ b/src/Nexus.Simulator/Rendering/SimRenderer.cs @@ -176,7 +176,8 @@ public class SimRenderer if (state is not null) { - drawList.AddText(textPos, color, $"Danger: {state.Danger} Enemies: {state.HostileMonsters.Count}"); + var ta = state.ThreatAssessment; + drawList.AddText(textPos, color, $"Threat: {ta.ZoneThreatLevel:F0} (fw={ta.FleeWeight:F1}) Enemies: {state.HostileMonsters.Count}"); textPos.Y += 16; } diff --git a/src/Nexus.Systems/ThreatSystem.cs b/src/Nexus.Systems/ThreatSystem.cs index 7810879..7dacac3 100644 --- a/src/Nexus.Systems/ThreatSystem.cs +++ b/src/Nexus.Systems/ThreatSystem.cs @@ -5,9 +5,9 @@ using Serilog; namespace Nexus.Systems; /// -/// Emergency-only threat response. Runs first (priority 50). -/// Only fires on Critical danger (low HP or overwhelming threat score). -/// Normal combat (High/Medium) is handled by MovementSystem orbiting + CombatSystem herding. +/// Per-entity threat scoring with continuous flee weights. +/// Runs first (priority 50). Builds ThreatAssessment on GameState, +/// then submits flee movement intents when warranted. /// public class ThreatSystem : ISystem { @@ -15,54 +15,440 @@ public class ThreatSystem : ISystem public string Name => "Threat"; public bool IsEnabled { get; set; } = true; - /// If closest enemy is within this range AND danger is Critical, escalate to urgent flee. - public float PointBlankRange { get; set; } = 120f; - - /// World-to-grid conversion factor for terrain queries. public float WorldToGrid { get; set; } = 23f / 250f; - private DangerLevel _prevDanger = DangerLevel.Safe; + // ── Config ── + public float MaxThreatRange { get; set; } = 900f; + public float PointBlankRange { get; set; } = 120f; + public float FleeThreshold { get; set; } = 180f; + public float PanicThreshold { get; set; } = 350f; + + // Weights for score composition + private const float W_Distance = 1.0f; + private const float W_Rarity = 1.0f; + private const float W_PackSize = 0.3f; + + // Score decay for smooth transitions + private float _smoothedZoneThreat; + private const float ZoneThreatUpAlpha = 0.12f; // ramp up over ~8 ticks (~130ms) + private const float ZoneThreatDownAlpha = 0.04f; // decay over ~25 ticks (~400ms) + + // Top-threat debounce — locked entity must lose for N consecutive ticks before switching + private uint? _lockedTopThreatId; + private int _loseStreak; + private const int TopThreatDebounce = 20; // ~330ms at 60Hz + + // Kill-target debounce + private uint? _lockedKillTargetId; + private int _killLoseStreak; + private const int KillTargetDebounce = 15; // ~250ms + + // Logging + private ThreatCategory _prevMaxCategory = ThreatCategory.Ignore; + private uint? _prevTopThreatId; public void Update(GameState state, ActionQueue actions, MovementBlender movement) { if (!state.Player.HasPosition) return; - var danger = state.Danger; - var threats = state.Threats; + var playerPos = state.Player.Position; + var playerHpFactor = 1f + (1f - state.Player.LifePercent / 100f) * 1.5f; - // Log danger transitions - if (danger != _prevDanger) + // ── 1. Score each hostile ── + var entries = new List(state.HostileMonsters.Count); + + foreach (var monster in state.HostileMonsters) { - if (danger >= DangerLevel.High) - Log.Warning("Threat: {Prev} -> {Cur} (hostiles={Total}, close={Close}, closest={Dist:F0})", - _prevDanger, danger, threats.TotalHostiles, threats.CloseRange, threats.ClosestDistance); - else - Log.Debug("Threat: {Prev} -> {Cur}", _prevDanger, danger); - _prevDanger = danger; + if (!monster.IsAlive) continue; + + var entry = ScoreEntity(monster, playerPos, playerHpFactor, state); + entries.Add(entry); } - // Only respond to Critical danger — High is normal combat, handled by orbit/herd - if (danger != DangerLevel.Critical) return; - if (threats.TotalHostiles == 0) return; - - // Compute flee direction: away from threat centroid - var fleeDir = state.Player.Position - threats.ThreatCentroid; - if (fleeDir.LengthSquared() < 0.0001f) - fleeDir = Vector2.UnitY; - - fleeDir = Vector2.Normalize(fleeDir); - - var isPointBlank = threats.ClosestDistance < PointBlankRange; - - if (isPointBlank) + // ── 2. Pack context pass — count nearby allies per monster (capped) ── + for (var i = 0; i < entries.Count; i++) { - // Layer 0: total override — pure flee, blocks casting - movement.Submit(new MovementIntent(0, fleeDir, 1.0f, "Threat")); + var nearby = 0; + for (var j = 0; j < entries.Count; j++) + { + if (i == j) continue; + if (Vector2.DistanceSquared(entries[i].Position, entries[j].Position) < 600f * 600f) + nearby++; + } + // Cap pack bonus at 5 allies so 50-mob groups don't dominate the score + entries[i].ThreatScore += Math.Min(nearby, 5) * W_PackSize; + } + + // ── 3. Classify each entry ── + var anyEmergency = false; + ThreatEntry? rawTopThreat = null; + ThreatEntry? lockedEntry = null; + var closestDist = 0f; + var closestFound = false; + + foreach (var entry in entries) + { + entry.Category = Classify(entry, state.Player); + entry.PerceivedDanger = Math.Clamp(entry.ThreatScore / PanicThreshold, 0f, 1f); + + if (entry.Category == ThreatCategory.Emergency) + anyEmergency = true; + + if (rawTopThreat is null || entry.ThreatScore > rawTopThreat.ThreatScore + || (entry.ThreatScore == rawTopThreat.ThreatScore && entry.EntityId < rawTopThreat.EntityId)) + rawTopThreat = entry; + + if (_lockedTopThreatId.HasValue && entry.EntityId == _lockedTopThreatId.Value) + lockedEntry = entry; + + if (!closestFound || entry.DistanceToPlayer < closestDist) + { + closestDist = entry.DistanceToPlayer; + closestFound = true; + } + } + + // Debounce top-threat: locked entity stays until it's clearly outclassed for N ticks + ThreatEntry? mostDangerous; + if (lockedEntry is null) + { + // Locked entity is dead/despawned — accept raw winner immediately + mostDangerous = rawTopThreat; + _lockedTopThreatId = rawTopThreat?.EntityId; + _loseStreak = 0; + } + else if (rawTopThreat is not null && rawTopThreat.EntityId != _lockedTopThreatId) + { + // Only count as "losing" if the locked entry is significantly behind (>30%) + var significantlyBehind = rawTopThreat.ThreatScore > lockedEntry.ThreatScore * 1.3f; + if (significantlyBehind) + _loseStreak++; + // Still count slow drift if locked is behind at all, but at half rate + else if (rawTopThreat.ThreatScore > lockedEntry.ThreatScore) + _loseStreak = Math.Max(0, _loseStreak); // don't reset, just don't increment + else + _loseStreak = Math.Max(0, _loseStreak - 1); // locked is actually winning, cool down + + if (_loseStreak >= TopThreatDebounce) + { + mostDangerous = rawTopThreat; + _lockedTopThreatId = rawTopThreat.EntityId; + _loseStreak = 0; + } + else + { + mostDangerous = lockedEntry; + } } else { - // Layer 1: strong flee but allow some nav/orbit bleed-through - movement.Submit(new MovementIntent(1, fleeDir, 0.6f, "Threat")); + // Locked entity is still the raw winner — cool down streak + mostDangerous = lockedEntry; + _loseStreak = Math.Max(0, _loseStreak - 2); // cool down faster when winning + } + + // ── Log top threat changes ── + var newTopId = mostDangerous?.EntityId; + if (newTopId != _prevTopThreatId) + { + if (mostDangerous is not null) + Log.Information("TopThreat: #{Id} ({Rarity}) score={Score:F1} cat={Cat} dist={Dist:F0} hp={Hp:P0} (prev=#{Prev})", + mostDangerous.EntityId, mostDangerous.Rarity, mostDangerous.ThreatScore, + mostDangerous.Category, mostDangerous.DistanceToPlayer, mostDangerous.HpPercent, + _prevTopThreatId?.ToString() ?? "none"); + else + Log.Information("TopThreat: cleared (prev=#{Prev})", _prevTopThreatId?.ToString() ?? "none"); + + _prevTopThreatId = newTopId; + } + + // ── 4. Aggregate into ThreatAssessment ── + var rawZone = 0f; + foreach (var e in entries) + rawZone += e.ThreatScore; + + // Smooth zone threat — fast up, slow down + if (rawZone >= _smoothedZoneThreat) + _smoothedZoneThreat += (rawZone - _smoothedZoneThreat) * ZoneThreatUpAlpha; + else + _smoothedZoneThreat += (rawZone - _smoothedZoneThreat) * ZoneThreatDownAlpha; + + var centroid = ComputeThreatCentroid(entries, playerPos); + var safest = playerPos - centroid; + if (safest.LengthSquared() < 0.0001f) safest = Vector2.UnitY; + safest = Vector2.Normalize(safest); + + // Hysteresis on flee transition — require score to drop 15% below threshold to de-escalate + var wasFleeing = _prevMaxCategory >= ThreatCategory.Flee; + var fleeOffThreshold = wasFleeing ? FleeThreshold * 0.85f : FleeThreshold; + var shouldFlee = _smoothedZoneThreat > fleeOffThreshold || anyEmergency; + var areaClear = entries.TrueForAll(e => e.Category < ThreatCategory.Monitor); + + // Range band counts (backward compat) + int close = 0, mid = 0, far = 0; + bool hasRareOrUnique = false; + foreach (var e in entries) + { + if (e.DistanceToPlayer < 300f) close++; + else if (e.DistanceToPlayer < 600f) mid++; + else if (e.DistanceToPlayer < 1200f) far++; + if (e.Rarity >= MonsterRarity.Rare) hasRareOrUnique = true; + } + + var assessment = new ThreatAssessment + { + Entries = entries, + ZoneThreatLevel = _smoothedZoneThreat, + PrimaryTarget = SelectPrimaryTarget(entries), + MostDangerous = mostDangerous, + ThreatCentroid = centroid, + SafestDirection = safest, + AnyEmergency = anyEmergency, + ShouldFlee = shouldFlee, + AreaClear = areaClear, + ClosestDistance = closestDist, + FleeWeight = Math.Clamp(_smoothedZoneThreat / PanicThreshold, 0f, 1f), + CloseRange = close, + MidRange = mid, + FarRange = far, + HasRareOrUnique = hasRareOrUnique, + }; + + // Debounce kill target — same lose-streak pattern + var rawKillTarget = assessment.PrimaryTarget; + var lockedKillEntry = _lockedKillTargetId.HasValue + ? entries.FirstOrDefault(e => e.EntityId == _lockedKillTargetId.Value + && e.Category >= ThreatCategory.Engage && e.HasLineOfSight) + : null; + + if (lockedKillEntry is null) + { + _lockedKillTargetId = rawKillTarget?.EntityId; + _killLoseStreak = 0; + } + else if (rawKillTarget is not null && rawKillTarget.EntityId != _lockedKillTargetId) + { + _killLoseStreak++; + if (_killLoseStreak >= KillTargetDebounce) + { + _lockedKillTargetId = rawKillTarget.EntityId; + _killLoseStreak = 0; + } + else + { + assessment.PrimaryTarget = lockedKillEntry; + } + } + else + { + _killLoseStreak = 0; + } + + state.ThreatAssessment = assessment; + + // Backward compat — keep DangerLevel for consumers that still read it + state.Danger = ToDangerLevel(assessment); + state.Threats = new ThreatMap + { + TotalHostiles = entries.Count, + CloseRange = close, + MidRange = mid, + FarRange = far, + ClosestDistance = closestDist, + ThreatCentroid = centroid, + HasRareOrUnique = hasRareOrUnique, + }; + + // ── 5. Log zone-level transitions with hysteresis ── + // Each threshold has a lower de-escalation point (15% below) to prevent bouncing + var zoneCat = ClassifyZone(_smoothedZoneThreat, anyEmergency, shouldFlee, _prevMaxCategory); + + if (zoneCat != _prevMaxCategory) + { + if (zoneCat >= ThreatCategory.Flee) + Log.Warning("Threat: {Prev} -> {Cur} (zone={Zone:F1}, closest={Dist:F0}, hostiles={Count})", + _prevMaxCategory, zoneCat, _smoothedZoneThreat, closestDist, entries.Count); + else + Log.Debug("Threat: {Prev} -> {Cur} (zone={Zone:F1})", + _prevMaxCategory, zoneCat, _smoothedZoneThreat); + _prevMaxCategory = zoneCat; + } + + // ── 6. Submit movement intents ── + if (!shouldFlee) return; + + var isPointBlank = closestDist < PointBlankRange; + + if (anyEmergency || isPointBlank) + { + // Layer 0: near-total override — flee, blocks casting. 0.85 lets wall push still help. + movement.Submit(new MovementIntent(0, safest, 0.85f, "Threat")); + } + else + { + // Layer 1: strong flee scaled by flee weight + var override1 = 0.3f + assessment.FleeWeight * 0.4f; // 0.3–0.7 + movement.Submit(new MovementIntent(1, safest * assessment.FleeWeight, override1, "Threat")); } } + + // ── Per-entity scoring ── + + private ThreatEntry ScoreEntity(EntitySnapshot monster, Vector2 playerPos, float playerHpFactor, GameState state) + { + var dist = monster.DistanceToPlayer; + var score = 0f; + + // Distance — nonlinear: spikes sharply as enemies close in + var distFactor = DistanceFactor(dist); + score += distFactor * W_Distance; + + // Rarity — scaled by distance so far-away rares/uniques don't dominate + var rarityBase = monster.Rarity switch + { + MonsterRarity.Unique => 3f, + MonsterRarity.Rare => 2f, + MonsterRarity.Magic => 1.3f, + _ => 1f, + }; + // At max range: rarity contributes 20% of its base; at close range: 100% + var rarityScale = 0.2f + 0.8f * Math.Clamp(distFactor / 10f, 0f, 1f); + score += rarityBase * rarityScale * W_Rarity; + + // LOS — de-weight monsters behind walls + var hasLos = true; + if (state.Terrain is { } terrain) + { + hasLos = TerrainQuery.HasLineOfSight(terrain, playerPos, monster.Position, WorldToGrid); + if (!hasLos) score *= 0.4f; + } + + // Low HP monsters are less threatening + var hpPct = monster.LifeTotal > 0 ? (float)monster.LifeCurrent / monster.LifeTotal : 1f; + if (hpPct < 0.1f) score *= 0.3f; + + // Player HP context — same monster is scarier when you're low + score *= playerHpFactor; + + return new ThreatEntry + { + EntityId = monster.Id, + Position = monster.Position, + DistanceToPlayer = dist, + ThreatScore = score, + HasLineOfSight = hasLos, + Rarity = monster.Rarity, + HpPercent = hpPct, + IsAlive = true, + }; + } + + private float DistanceFactor(float dist) + { + if (dist > MaxThreatRange) return 0f; + var t = 1f - dist / MaxThreatRange; + return t * t * 10f; // inverse square: 10 @ 0, 2.5 @ 450, 0 @ 900 + } + + // ── Classification ── + + private ThreatCategory Classify(ThreatEntry entry, PlayerState player) + { + // Emergency overrides — certain conditions always trigger + if (player.LifePercent < 25f && entry.ThreatScore > 8f) + return ThreatCategory.Emergency; + + return entry.ThreatScore switch + { + > 20f => ThreatCategory.Flee, + > 8f => ThreatCategory.Engage, + > 3f => ThreatCategory.Monitor, + > 0f => ThreatCategory.Ignore, + _ => ThreatCategory.Ignore, + }; + } + + // ── Centroid (score-weighted) ── + + private static Vector2 ComputeThreatCentroid(List entries, Vector2 playerPos) + { + var totalWeight = 0f; + var weighted = Vector2.Zero; + + foreach (var e in entries) + { + if (e.Category < ThreatCategory.Monitor) continue; + weighted += e.Position * e.ThreatScore; + totalWeight += e.ThreatScore; + } + + return totalWeight > 0f ? weighted / totalWeight : playerPos; + } + + // ── Target selection (kill priority, not raw threat) ── + + private static ThreatEntry? SelectPrimaryTarget(List entries) + { + ThreatEntry? best = null; + var bestScore = float.MinValue; + + foreach (var e in entries) + { + if (e.Category < ThreatCategory.Engage) continue; + if (!e.HasLineOfSight) continue; + + var score = 0f; + + // Prefer low HP targets — finish them off + score += (1f - e.HpPercent) * 3f; + + // Prefer closer targets + score += (1f - Math.Clamp(e.DistanceToPlayer / 800f, 0f, 1f)) * 2f; + + // Prefer dangerous rarity + if (e.Rarity >= MonsterRarity.Rare) score += 2f; + if (e.Rarity == MonsterRarity.Unique) score += 3f; + + if (score > bestScore) + { + bestScore = score; + best = e; + } + } + + return best; + } + + // ── Backward compat ── + + private static ThreatCategory ClassifyZone(float zone, bool anyEmergency, bool shouldFlee, ThreatCategory prev) + { + if (anyEmergency) return ThreatCategory.Emergency; + if (shouldFlee) return ThreatCategory.Flee; + + // Escalation thresholds / de-escalation thresholds (15% gap) + // Engage: up at 100, down at 85 + // Monitor: up at 30, down at 25 + // Ignore: up at 5 + return prev switch + { + ThreatCategory.Engage when zone >= 85f => ThreatCategory.Engage, + ThreatCategory.Monitor when zone >= 100f => ThreatCategory.Engage, + ThreatCategory.Monitor when zone >= 25f => ThreatCategory.Monitor, + _ when zone >= 100f => ThreatCategory.Engage, + _ when zone >= 30f => ThreatCategory.Monitor, + _ when zone > 5f => ThreatCategory.Ignore, + _ => ThreatCategory.Ignore, + }; + } + + private static DangerLevel ToDangerLevel(ThreatAssessment a) + { + if (a.AnyEmergency) return DangerLevel.Critical; + if (a.ShouldFlee) return DangerLevel.Critical; + if (a.FleeWeight > 0.5f) return DangerLevel.High; + if (a.FleeWeight > 0.15f) return DangerLevel.Medium; + if (a.ZoneThreatLevel > 5f) return DangerLevel.Low; + return DangerLevel.Safe; + } } From 8ca257bc79d5e1a32dd354978f596b1bfa2e47b9 Mon Sep 17 00:00:00 2001 From: Boki Date: Sat, 7 Mar 2026 14:38:20 -0500 Subject: [PATCH 4/5] test --- imgui.ini | 4 +- src/Nexus.Core/BotConfig.cs | 3 + src/Nexus.Core/MovementBlender.cs | 38 ++- src/Nexus.Core/TerrainQuery.cs | 60 +++- src/Nexus.Pathfinding/NavigationController.cs | 2 + src/Nexus.Pathfinding/PathFinder.cs | 9 + src/Nexus.Simulator/Config/SimConfig.cs | 16 +- src/Nexus.Simulator/Program.cs | 11 +- src/Nexus.Simulator/Rendering/SimRenderer.cs | 8 +- .../Rendering/TerrainRenderer.cs | 44 ++- src/Nexus.Simulator/World/SimWorld.cs | 175 +++++------ src/Nexus.Simulator/World/TerrainGenerator.cs | 277 ++++++++++++++++++ src/Nexus.Systems/BotTick.cs | 52 +++- src/Nexus.Systems/MovementSystem.cs | 23 +- src/Nexus.Systems/ThreatSystem.cs | 2 +- 15 files changed, 577 insertions(+), 147 deletions(-) diff --git a/imgui.ini b/imgui.ini index ef2059b..5af8267 100644 --- a/imgui.ini +++ b/imgui.ini @@ -9,7 +9,7 @@ Size=432,649 Collapsed=0 [Window][Simulator] -Pos=499,177 -Size=1200,681 +Pos=564,96 +Size=893,571 Collapsed=0 diff --git a/src/Nexus.Core/BotConfig.cs b/src/Nexus.Core/BotConfig.cs index eac93b7..bc304c0 100644 --- a/src/Nexus.Core/BotConfig.cs +++ b/src/Nexus.Core/BotConfig.cs @@ -14,6 +14,9 @@ public class BotConfig // Navigation public float WorldToGrid { get; set; } = 23f / 250f; + // Combat engagement — suppress navigation when enemies are within this range + public float CombatEngagementRange { get; set; } = 600f; + // Loot public float LootPickupRange { get; set; } = 600f; diff --git a/src/Nexus.Core/MovementBlender.cs b/src/Nexus.Core/MovementBlender.cs index 8fa46ed..87bce56 100644 --- a/src/Nexus.Core/MovementBlender.cs +++ b/src/Nexus.Core/MovementBlender.cs @@ -21,8 +21,10 @@ public sealed class MovementBlender // Stuck detection private Vector2 _lastResolvePos; private int _stuckFrames; - private const int StuckFrameThreshold = 30; // ~0.5s at 60Hz - private const float StuckMovePerFrame = 3f; // must move > 3 world units per frame to count as moving + private const int StuckFrameThreshold = 15; // ~250ms at 60Hz + private const int StuckRecoveryThreshold = 45; // ~750ms — try random direction to break free + private const float StuckMovePerFrame = 3f; // must move > 3 world units per frame to count as moving + private static readonly Random StuckRng = new(); // EMA smoothing to dampen terrain validation jitter. // Snap decision based on INTENT change (pre-terrain), not terrain output — prevents @@ -58,15 +60,11 @@ public sealed class MovementBlender public void Clear() => _intents.Clear(); /// - /// Blends all submitted intents and validates against terrain. - /// Applies EMA smoothing after terrain validation to dampen probe jitter. + /// Updates stuck detection based on player movement. Call BEFORE systems run + /// so that IsStuck is available for systems to check (e.g. MovementSystem suppresses orbit). /// - public void Resolve(WalkabilitySnapshot? terrain, Vector2 playerPos, float worldToGrid) + public void UpdateStuckState(Vector2 playerPos) { - IsUrgentFlee = false; - - // ── Stuck detection ── - // If player barely moves for ~0.5s, suppress orbit/herd so navigation can guide out var moved = Vector2.Distance(playerPos, _lastResolvePos); if (moved < StuckMovePerFrame) _stuckFrames++; @@ -74,11 +72,31 @@ public sealed class MovementBlender _stuckFrames = Math.Max(0, _stuckFrames - 3); // recover 3x faster than building up _lastResolvePos = playerPos; IsStuck = _stuckFrames > StuckFrameThreshold; + } + + /// + /// Blends all submitted intents and validates against terrain. + /// Applies EMA smoothing after terrain validation to dampen probe jitter. + /// + public void Resolve(WalkabilitySnapshot? terrain, Vector2 playerPos, float worldToGrid) + { + IsUrgentFlee = false; if (IsStuck) { - // Keep only flee (L0, L1) and navigation (L3) — drop orbit (L2) and herd (L4) + // Keep only flee (L0, L1), navigation (L3), and wall push (L5) — drop orbit (L2) and herd (L4) _intents.RemoveAll(i => i.Layer == 2 || i.Layer == 4); + + // After 1s stuck, inject a random perpendicular nudge to break free + if (_stuckFrames > StuckRecoveryThreshold) + { + var angle = StuckRng.NextDouble() * Math.PI * 2; + var nudge = new System.Numerics.Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + _intents.Add(new MovementIntent(1, nudge, 0.7f, "StuckEscape")); + // Reset counter so we try a new direction periodically + if (_stuckFrames % 30 == 0) + _stuckFrames = StuckRecoveryThreshold + 1; + } } _lastIntents = new List(_intents); diff --git a/src/Nexus.Core/TerrainQuery.cs b/src/Nexus.Core/TerrainQuery.cs index e6237fc..06e9809 100644 --- a/src/Nexus.Core/TerrainQuery.cs +++ b/src/Nexus.Core/TerrainQuery.cs @@ -88,8 +88,8 @@ public static class TerrainQuery /// public static Vector2 ComputeWallRepulsion(WalkabilitySnapshot terrain, Vector2 playerPos, float worldToGrid) { - const float probeNear = 25f; // ~2-3 grid cells - const float probeFar = 60f; // ~5-6 grid cells + const float probeNear = 40f; // ~3-4 grid cells + const float probeFar = 100f; // ~9-10 grid cells var push = Vector2.Zero; @@ -122,6 +122,62 @@ public static class TerrainQuery return Vector2.Normalize(push); } + /// + /// Predictive wall steering — casts rays ahead along the movement direction. + /// If forward is blocked but a side is clear, returns a lateral steering vector. + /// + public static Vector2 ComputeWallSteering( + WalkabilitySnapshot terrain, Vector2 playerPos, Vector2 moveDir, float worldToGrid) + { + if (moveDir.LengthSquared() < 0.0001f) + return Vector2.Zero; + + var dir = Vector2.Normalize(moveDir); + var leftDir = Rotate(dir, 30f); + var rightDir = Rotate(dir, -30f); + + ReadOnlySpan distances = [40f, 80f, 120f]; + + var forwardBlocked = false; + var leftClear = true; + var rightClear = true; + + foreach (var dist in distances) + { + var fwd = playerPos + dir * dist; + var fx = (int)(fwd.X * worldToGrid); + var fy = (int)(fwd.Y * worldToGrid); + if (!terrain.IsWalkable(fx, fy)) + forwardBlocked = true; + + var left = playerPos + leftDir * dist; + var lx = (int)(left.X * worldToGrid); + var ly = (int)(left.Y * worldToGrid); + if (!terrain.IsWalkable(lx, ly)) + leftClear = false; + + var right = playerPos + rightDir * dist; + var rx = (int)(right.X * worldToGrid); + var ry = (int)(right.Y * worldToGrid); + if (!terrain.IsWalkable(rx, ry)) + rightClear = false; + } + + if (!forwardBlocked) + return Vector2.Zero; + + // Steer toward the clear side + var lateral = new Vector2(-dir.Y, dir.X); // perpendicular (left) + if (leftClear && !rightClear) + return lateral; + if (rightClear && !leftClear) + return -lateral; + if (leftClear && rightClear) + return lateral; // default left when both clear + // Both blocked — push backward + return -dir; + } + private static Vector2 Rotate(Vector2 v, float degrees) { float rad = degrees * MathF.PI / 180f; diff --git a/src/Nexus.Pathfinding/NavigationController.cs b/src/Nexus.Pathfinding/NavigationController.cs index b24f646..ea929d3 100644 --- a/src/Nexus.Pathfinding/NavigationController.cs +++ b/src/Nexus.Pathfinding/NavigationController.cs @@ -218,6 +218,8 @@ public sealed class NavigationController return; } Log.Debug("NavigationController: stuck detected, repathing"); + _positionHistory.Clear(); + _stuckGraceTicks = 120; // 2 seconds grace before next stuck check } } diff --git a/src/Nexus.Pathfinding/PathFinder.cs b/src/Nexus.Pathfinding/PathFinder.cs index c29fdf5..5c3ba60 100644 --- a/src/Nexus.Pathfinding/PathFinder.cs +++ b/src/Nexus.Pathfinding/PathFinder.cs @@ -105,6 +105,15 @@ public static class PathFinder stepCost *= 1.5f; } + // Wall-proximity penalty — prefer corridor centers + var wallCount = 0; + for (var d = 0; d < 8; d++) + { + if (!terrain.IsWalkable(nx + Dx[d], ny + Dy[d])) + wallCount++; + } + stepCost += wallCount * 0.5f; + var tentativeG = currentG + stepCost; if (tentativeG < gScore.GetValueOrDefault(neighbor, float.MaxValue)) diff --git a/src/Nexus.Simulator/Config/SimConfig.cs b/src/Nexus.Simulator/Config/SimConfig.cs index 567f085..4724343 100644 --- a/src/Nexus.Simulator/Config/SimConfig.cs +++ b/src/Nexus.Simulator/Config/SimConfig.cs @@ -3,10 +3,18 @@ namespace Nexus.Simulator.Config; public class SimConfig { // Terrain - public int TerrainWidth { get; set; } = 500; - public int TerrainHeight { get; set; } = 500; + public int TerrainWidth { get; set; } = 1500; + public int TerrainHeight { get; set; } = 1500; public float WorldToGrid { get; set; } = 23f / 250f; + // Dungeon generation + public int DungeonRoomCountMin { get; set; } = 10; + public int DungeonRoomCountMax { get; set; } = 18; + public int DungeonRoomSizeMin { get; set; } = 55; // grid cells (~600 world units) + public int DungeonRoomSizeMax { get; set; } = 110; // grid cells (~1200 world units) + public int DungeonCorridorWidth { get; set; } = 13; // grid cells (~140 world units) + public float DungeonEndReachDist { get; set; } = 150f; // world units + // Player public float PlayerMoveSpeed { get; set; } = 400f; public int PlayerMaxHealth { get; set; } = 800; @@ -55,10 +63,6 @@ public class SimConfig public float ProjectileHitRadius { get; set; } = 80f; public int SkillBaseDamage { get; set; } = 200; - // Terrain expansion - public int ExpandThreshold { get; set; } = 50; - public int ExpandAmount { get; set; } = 250; - // Simulation public float SpeedMultiplier { get; set; } = 1f; public bool IsPaused { get; set; } diff --git a/src/Nexus.Simulator/Program.cs b/src/Nexus.Simulator/Program.cs index 891338d..76b8a01 100644 --- a/src/Nexus.Simulator/Program.cs +++ b/src/Nexus.Simulator/Program.cs @@ -72,8 +72,8 @@ foreach (var sys in systems) // ── Start simulation poller ── poller.Start(); -// ── Start exploring ── -nav.Explore(); +// ── Navigate to dungeon end ── +nav.NavigateTo(world.EndWorldPos); // ── Bot logic thread ── var actionQueue = new ActionQueue(); @@ -98,6 +98,13 @@ var botThread = new Thread(() => var resolved = BotTick.Run(state, systems, actionQueue, movementBlender, nav, botConfig); ActionExecutor.Execute(resolved, input, moveTracker, movementBlender, state.Player.Position); + // Check if dungeon end reached — regenerate and re-navigate + if (world.ReachedEnd) + { + world.RegenerateTerrain(); + nav.NavigateTo(world.EndWorldPos); + } + botTickCount++; if (maxTicks.HasValue && botTickCount >= maxTicks.Value) { diff --git a/src/Nexus.Simulator/Rendering/SimRenderer.cs b/src/Nexus.Simulator/Rendering/SimRenderer.cs index 1b1a344..0338ea6 100644 --- a/src/Nexus.Simulator/Rendering/SimRenderer.cs +++ b/src/Nexus.Simulator/Rendering/SimRenderer.cs @@ -65,7 +65,8 @@ public class SimRenderer // 1. Terrain TerrainRenderer.Draw(drawList, _world.Terrain, vt, canvasSize, _nav.ExploredGrid, _nav.ExploredWidth, _nav.ExploredHeight, - _nav.ExploredOffsetX, _nav.ExploredOffsetY); + _nav.ExploredOffsetX, _nav.ExploredOffsetY, + _world.StartWorldPos, _world.EndWorldPos); // 2. Path PathRenderer.Draw(drawList, _nav, vt); @@ -93,7 +94,10 @@ public class SimRenderer var minimapSize = 150f; var minimapOrigin = canvasOrigin + canvasSize - new Vector2(minimapSize + 10, minimapSize + 10); var playerGridPos = _world.Player.Position * _config.WorldToGrid; - TerrainRenderer.DrawMinimap(drawList, _world.Terrain, playerGridPos, minimapOrigin, minimapSize); + var startGridPos = _world.StartWorldPos * _config.WorldToGrid; + var endGridPos = _world.EndWorldPos * _config.WorldToGrid; + TerrainRenderer.DrawMinimap(drawList, _world.Terrain, playerGridPos, minimapOrigin, minimapSize, + startGridPos, endGridPos); // HUD text DrawHud(drawList, canvasOrigin, state); diff --git a/src/Nexus.Simulator/Rendering/TerrainRenderer.cs b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs index b68d76c..edf1ea1 100644 --- a/src/Nexus.Simulator/Rendering/TerrainRenderer.cs +++ b/src/Nexus.Simulator/Rendering/TerrainRenderer.cs @@ -13,7 +13,8 @@ public static class TerrainRenderer public static void Draw(ImDrawListPtr drawList, WalkabilitySnapshot terrain, ViewTransform vt, Vector2 canvasSize, bool[]? exploredGrid = null, int exploredWidth = 0, int exploredHeight = 0, - int exploredOffsetX = 0, int exploredOffsetY = 0) + int exploredOffsetX = 0, int exploredOffsetY = 0, + Vector2? startWorldPos = null, Vector2? endWorldPos = null) { var cellSize = vt.Zoom; if (cellSize < 0.5f) return; @@ -71,13 +72,32 @@ public static class TerrainRenderer var p3 = vt.GridToScreen(gx, gy + step); // left drawList.AddQuadFilled(p0, p1, p2, p3, color); } + + // Draw start marker (green circle) + if (startWorldPos.HasValue) + { + var sp = vt.WorldToScreen(startWorldPos.Value); + var r = Math.Max(6f, vt.Zoom * 3f); + drawList.AddCircleFilled(sp, r, 0xFF00FF00); // green + drawList.AddCircle(sp, r, 0xFF00AA00, 0, 2f); + } + + // Draw end marker (red circle) + if (endWorldPos.HasValue) + { + var ep = vt.WorldToScreen(endWorldPos.Value); + var r = Math.Max(6f, vt.Zoom * 3f); + drawList.AddCircleFilled(ep, r, 0xFF0000FF); // red (ABGR) + drawList.AddCircle(ep, r, 0xFF0000AA, 0, 2f); + } } /// /// Draws a minimap in the corner (top-down, no rotation). /// public static void DrawMinimap(ImDrawListPtr drawList, WalkabilitySnapshot terrain, - Vector2 playerGridPos, Vector2 minimapOrigin, float minimapSize) + Vector2 playerGridPos, Vector2 minimapOrigin, float minimapSize, + Vector2? startGridPos = null, Vector2? endGridPos = null) { var scaleX = minimapSize / terrain.Width; var scaleY = minimapSize / terrain.Height; @@ -102,10 +122,26 @@ public static class TerrainRenderer 0xFF2A2A3F); } - // Player dot — convert absolute grid pos to local + // Start marker (green dot) + if (startGridPos.HasValue) + { + var startLocal = startGridPos.Value - new Vector2(terrain.OffsetX, terrain.OffsetY); + var startPx = minimapOrigin + startLocal * scale; + drawList.AddCircleFilled(startPx, 3f, 0xFF00FF00); // green + } + + // End marker (red dot) + if (endGridPos.HasValue) + { + var endLocal = endGridPos.Value - new Vector2(terrain.OffsetX, terrain.OffsetY); + var endPx = minimapOrigin + endLocal * scale; + drawList.AddCircleFilled(endPx, 3f, 0xFF0000FF); // red + } + + // Player dot (cyan) — convert absolute grid pos to local var playerLocalPos = playerGridPos - new Vector2(terrain.OffsetX, terrain.OffsetY); var playerPx = minimapOrigin + playerLocalPos * scale; - drawList.AddCircleFilled(playerPx, 3f, 0xFF00FF00); + drawList.AddCircleFilled(playerPx, 3f, 0xFFFFFF00); // cyan (ABGR) // Border drawList.AddRect(minimapOrigin, diff --git a/src/Nexus.Simulator/World/SimWorld.cs b/src/Nexus.Simulator/World/SimWorld.cs index 1c9f858..681d295 100644 --- a/src/Nexus.Simulator/World/SimWorld.cs +++ b/src/Nexus.Simulator/World/SimWorld.cs @@ -16,6 +16,11 @@ public class SimWorld public List ActiveEffects { get; } = []; public WalkabilitySnapshot Terrain { get; private set; } public long TickNumber { get; private set; } + public Vector2 StartWorldPos { get; private set; } + public Vector2 EndWorldPos { get; private set; } + public bool ReachedEnd { get; private set; } + + private int _dungeonSeed; // Pending respawns private readonly List<(float timer, MonsterRarity rarity)> _respawnQueue = []; @@ -28,29 +33,45 @@ public class SimWorld public SimWorld(SimConfig config) { _config = config; - Terrain = TerrainGenerator.Generate(config.TerrainWidth, config.TerrainHeight); + _dungeonSeed = _rng.Next(); + + var dungeon = TerrainGenerator.GenerateDungeon( + config.TerrainWidth, config.TerrainHeight, _dungeonSeed, + config.DungeonRoomCountMin, config.DungeonRoomCountMax, + config.DungeonRoomSizeMin, config.DungeonRoomSizeMax, + config.DungeonCorridorWidth, config.WorldToGrid); + + Terrain = dungeon.Terrain; + StartWorldPos = dungeon.StartWorldPos; + EndWorldPos = dungeon.EndWorldPos; - // Spawn player at center - var gridToWorld = 1f / config.WorldToGrid; - var (sx, sy) = TerrainGenerator.FindSpawnPosition(Terrain); Player = new SimPlayer( config.PlayerMaxHealth, config.PlayerMaxMana, config.PlayerMaxEs, config.PlayerMoveSpeed, config.PlayerHealthRegen, config.PlayerManaRegen, config.PlayerEsRegen, config.PlayerEsRechargeDelay) { - Position = new Vector2(sx * gridToWorld, sy * gridToWorld), + Position = dungeon.StartWorldPos, }; - // Spawn initial enemies - SpawnEnemies(config.TargetEnemyCount); + // Spawn enemies inside dungeon rooms + SpawnEnemiesInRooms(dungeon.Rooms); } public void RegenerateTerrain() { - Terrain = TerrainGenerator.Generate(_config.TerrainWidth, _config.TerrainHeight); - var gridToWorld = 1f / _config.WorldToGrid; - var (sx, sy) = TerrainGenerator.FindSpawnPosition(Terrain); - Player.Position = new Vector2(sx * gridToWorld, sy * gridToWorld); + _dungeonSeed = _rng.Next(); + var dungeon = TerrainGenerator.GenerateDungeon( + _config.TerrainWidth, _config.TerrainHeight, _dungeonSeed, + _config.DungeonRoomCountMin, _config.DungeonRoomCountMax, + _config.DungeonRoomSizeMin, _config.DungeonRoomSizeMax, + _config.DungeonCorridorWidth, _config.WorldToGrid); + + Terrain = dungeon.Terrain; + StartWorldPos = dungeon.StartWorldPos; + EndWorldPos = dungeon.EndWorldPos; + ReachedEnd = false; + + Player.Position = dungeon.StartWorldPos; Player.Health = Player.MaxHealth; Player.Mana = Player.MaxMana; Player.Es = Player.MaxEs; @@ -58,7 +79,8 @@ public class SimWorld Projectiles.Clear(); ActiveEffects.Clear(); _respawnQueue.Clear(); - SpawnEnemies(_config.TargetEnemyCount); + SpawnEnemiesInRooms(dungeon.Rooms); + Log.Information("Dungeon regenerated (seed={Seed}), {Rooms} rooms", _dungeonSeed, dungeon.Rooms.Count); } public void QueueSkill(ushort scanCode, Vector2 targetWorldPos) @@ -73,8 +95,12 @@ public class SimWorld dt *= _config.SpeedMultiplier; TickNumber++; - // 0. Expand terrain if player near edge - CheckAndExpandTerrain(); + // 0. Check if player reached dungeon end + if (!ReachedEnd && Vector2.Distance(Player.Position, EndWorldPos) < _config.DungeonEndReachDist) + { + ReachedEnd = true; + Log.Information("Reached dungeon end!"); + } // 1. Move player MovePlayer(dt); @@ -508,26 +534,20 @@ public class SimWorld var (timer, rarity) = _respawnQueue[i]; timer -= dt; if (timer <= 0) + { + // Respawn at random walkable position in dungeon + var pos = TerrainGenerator.FindRandomWalkable(Terrain, _rng); + if (pos.HasValue) + { + var gridToWorld = 1f / _config.WorldToGrid; + SpawnEnemyAt(new Vector2(pos.Value.x * gridToWorld, pos.Value.y * gridToWorld), rarity); + } _respawnQueue.RemoveAt(i); + } else + { _respawnQueue[i] = (timer, rarity); - } - - // Cull enemies too far from player - for (var i = Enemies.Count - 1; i >= 0; i--) - { - var dist = Vector2.Distance(Enemies[i].Position, Player.Position); - if (dist > _config.EnemyCullDist) - Enemies.RemoveAt(i); - } - - // Maintain population - var aliveCount = Enemies.Count(e => e.IsAlive); - while (aliveCount < _config.TargetEnemyCount) - { - var spawned = SpawnGroup(RollRarity()); - aliveCount += spawned; - if (spawned == 0) break; + } } } @@ -548,91 +568,30 @@ public class SimWorld Enemies.Add(enemy); } - private void SpawnEnemies(int count) + private void SpawnEnemiesInRooms(List<(int x, int y, int w, int h)> rooms) { - while (count > 0) + var gridToWorld = 1f / _config.WorldToGrid; + var totalSpawned = 0; + + foreach (var (rx, ry, rw, rh) in rooms) { - var spawned = SpawnGroup(RollRarity()); - if (spawned == 0) break; - count -= spawned; - } - } + var roomCenter = new Vector2((rx + rw / 2f) * gridToWorld, (ry + rh / 2f) * gridToWorld); - private int SpawnGroup(MonsterRarity leaderRarity) - { - var center = FindSpawnNearPlayer(); - if (center is null) return 0; + // Skip the start room — don't spawn enemies right on the player + if (Vector2.Distance(roomCenter, StartWorldPos) < 50f) continue; - var groupSize = _rng.Next(_config.EnemyGroupMin, _config.EnemyGroupMax + 1); - var spawned = 0; - - for (var i = 0; i < groupSize; i++) - { - var rarity = i == 0 ? leaderRarity : MonsterRarity.White; - var offset = i == 0 - ? Vector2.Zero - : new Vector2( - (_rng.NextSingle() - 0.5f) * 2f * _config.EnemyGroupSpread, - (_rng.NextSingle() - 0.5f) * 2f * _config.EnemyGroupSpread); - - var pos = center.Value + offset; - var gx = (int)(pos.X * _config.WorldToGrid); - var gy = (int)(pos.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(gx, gy)) + var groupSize = _rng.Next(_config.EnemyGroupMin, _config.EnemyGroupMax + 1); + for (var i = 0; i < groupSize && totalSpawned < _config.TargetEnemyCount; i++) { + var lx = rx + _rng.Next(rw); + var ly = ry + _rng.Next(rh); + if (!Terrain.IsWalkable(lx, ly)) continue; + var pos = new Vector2(lx * gridToWorld, ly * gridToWorld); + var rarity = i == 0 ? RollRarity() : MonsterRarity.White; SpawnEnemyAt(pos, rarity); - spawned++; + totalSpawned++; } } - - return spawned; - } - - private Vector2? FindSpawnNearPlayer() - { - var baseAngle = MoveDirection.LengthSquared() > 0.01f - ? MathF.Atan2(MoveDirection.Y, MoveDirection.X) - : _rng.NextSingle() * MathF.Tau; - - for (var attempt = 0; attempt < 30; attempt++) - { - var angle = baseAngle + (_rng.NextSingle() - 0.5f) * MathF.PI; - var dist = _config.EnemySpawnMinDist + _rng.NextSingle() * (_config.EnemySpawnMaxDist - _config.EnemySpawnMinDist); - var pos = Player.Position + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * dist; - - var gx = (int)(pos.X * _config.WorldToGrid); - var gy = (int)(pos.Y * _config.WorldToGrid); - if (Terrain.IsWalkable(gx, gy)) - return pos; - } - - return null; - } - - private void CheckAndExpandTerrain() - { - var gx = (int)(Player.Position.X * _config.WorldToGrid); - var gy = (int)(Player.Position.Y * _config.WorldToGrid); - var t = Terrain; - - var distLeft = gx - t.OffsetX; - var distRight = (t.OffsetX + t.Width - 1) - gx; - var distTop = gy - t.OffsetY; - var distBottom = (t.OffsetY + t.Height - 1) - gy; - - var amt = _config.ExpandAmount; - var expandLeft = distLeft < _config.ExpandThreshold ? amt : 0; - var expandRight = distRight < _config.ExpandThreshold ? amt : 0; - var expandTop = distTop < _config.ExpandThreshold ? amt : 0; - var expandBottom = distBottom < _config.ExpandThreshold ? amt : 0; - - if (expandLeft > 0 || expandRight > 0 || expandTop > 0 || expandBottom > 0) - { - Terrain = TerrainGenerator.Expand(Terrain, expandLeft, expandRight, expandTop, expandBottom, _rng); - Serilog.Log.Information( - "Terrain expanded: {W}x{H} offset=({Ox},{Oy})", - Terrain.Width, Terrain.Height, Terrain.OffsetX, Terrain.OffsetY); - } } private MonsterRarity RollRarity() diff --git a/src/Nexus.Simulator/World/TerrainGenerator.cs b/src/Nexus.Simulator/World/TerrainGenerator.cs index 950b64b..0c7dc1e 100644 --- a/src/Nexus.Simulator/World/TerrainGenerator.cs +++ b/src/Nexus.Simulator/World/TerrainGenerator.cs @@ -3,6 +3,12 @@ using Nexus.Core; namespace Nexus.Simulator.World; +public record DungeonResult( + WalkabilitySnapshot Terrain, + Vector2 StartWorldPos, + Vector2 EndWorldPos, + List<(int x, int y, int w, int h)> Rooms); + public static class TerrainGenerator { // Permutation table for Perlin noise (fixed seed for deterministic terrain) @@ -22,6 +28,277 @@ public static class TerrainGenerator for (var i = 0; i < 512; i++) Perm[i] = p[i & 255]; } + public static DungeonResult GenerateDungeon(int width, int height, int seed, + int roomCountMin = 8, int roomCountMax = 15, + int roomSizeMin = 6, int roomSizeMax = 20, + int corridorWidth = 3, float worldToGrid = 23f / 250f) + { + var rng = new Random(seed); + var data = new byte[width * height]; // all walls by default + + // 1. Place rooms + var rooms = new List<(int x, int y, int w, int h)>(); + var targetCount = rng.Next(roomCountMin, roomCountMax + 1); + + for (var attempt = 0; attempt < targetCount * 50 && rooms.Count < targetCount; attempt++) + { + var rw = rng.Next(roomSizeMin, roomSizeMax + 1); + var rh = rng.Next(roomSizeMin, roomSizeMax + 1); + // Place rooms in the central third of the grid so they stay close together + var margin = width / 3; + var rx = rng.Next(margin, width - rw - margin); + var ry = rng.Next(margin, height - rh - margin); + + // Reject only if >50% overlap with an existing room (allow touching/partial overlap) + var overlaps = false; + foreach (var (ox, oy, ow, oh) in rooms) + { + var overlapX = Math.Max(0, Math.Min(rx + rw, ox + ow) - Math.Max(rx, ox)); + var overlapY = Math.Max(0, Math.Min(ry + rh, oy + oh) - Math.Max(ry, oy)); + var overlapArea = overlapX * overlapY; + var smallerArea = Math.Min(rw * rh, ow * oh); + if (overlapArea > smallerArea * 0.5f) + { + overlaps = true; + break; + } + } + if (overlaps) continue; + + rooms.Add((rx, ry, rw, rh)); + + // Carve room + for (var ly = ry; ly < ry + rh; ly++) + for (var lx = rx; lx < rx + rw; lx++) + data[ly * width + lx] = 1; + } + + if (rooms.Count < 2) + { + // Fallback: ensure at least 2 rooms + rooms.Clear(); + var r1 = (x: width / 4, y: height / 4, w: roomSizeMax, h: roomSizeMax); + var r2 = (x: width * 3 / 4 - roomSizeMax, y: height * 3 / 4 - roomSizeMax, w: roomSizeMax, h: roomSizeMax); + rooms.Add(r1); + rooms.Add(r2); + for (var ly = r1.y; ly < r1.y + r1.h; ly++) + for (var lx = r1.x; lx < r1.x + r1.w; lx++) + data[ly * width + lx] = 1; + for (var ly = r2.y; ly < r2.y + r2.h; ly++) + for (var lx = r2.x; lx < r2.x + r2.w; lx++) + data[ly * width + lx] = 1; + } + + // 2. Build MST using Prim's algorithm on room center distances + var roomCount = rooms.Count; + var inMst = new bool[roomCount]; + var mstEdges = new List<(int a, int b)>(); + inMst[0] = true; + + while (mstEdges.Count < roomCount - 1) + { + var bestDist = float.MaxValue; + var bestA = -1; + var bestB = -1; + + for (var a = 0; a < roomCount; a++) + { + if (!inMst[a]) continue; + for (var b = 0; b < roomCount; b++) + { + if (inMst[b]) continue; + var (ax, ay, aw, ah) = rooms[a]; + var (bx, by, bw, bh) = rooms[b]; + var dx = (ax + aw / 2f) - (bx + bw / 2f); + var dy = (ay + ah / 2f) - (by + bh / 2f); + var dist = dx * dx + dy * dy; + if (dist < bestDist) + { + bestDist = dist; + bestA = a; + bestB = b; + } + } + } + + if (bestB < 0) break; // disconnected (shouldn't happen) + inMst[bestB] = true; + mstEdges.Add((bestA, bestB)); + } + + // Add 1-2 extra edges for loops + var extraEdges = Math.Min(2, roomCount / 3); + for (var i = 0; i < extraEdges; i++) + { + var a = rng.Next(roomCount); + var b = rng.Next(roomCount); + if (a != b && !mstEdges.Contains((a, b)) && !mstEdges.Contains((b, a))) + mstEdges.Add((a, b)); + } + + // 3. Carve L-shaped corridors between connected rooms + foreach (var (a, b) in mstEdges) + { + var (ax, ay, aw, ah) = rooms[a]; + var (bx, by, bw, bh) = rooms[b]; + var cx1 = ax + aw / 2; + var cy1 = ay + ah / 2; + var cx2 = bx + bw / 2; + var cy2 = by + bh / 2; + + // Carve horizontal then vertical, with widened corner + CarveCorridor(data, width, height, cx1, cy1, cx2, cy1, corridorWidth); + CarveCorridor(data, width, height, cx2, cy1, cx2, cy2, corridorWidth); + + // Widen the corner with a square patch so the bot doesn't clip walls + var cornerR = corridorWidth; + for (var dy2 = -cornerR; dy2 <= cornerR; dy2++) + for (var dx2 = -cornerR; dx2 <= cornerR; dx2++) + { + var px = cx2 + dx2; + var py = cy1 + dy2; + if (px >= 0 && px < width && py >= 0 && py < height) + data[py * width + px] = 1; + } + } + + // 4. Build adjacency list from MST edges for BFS + var adj = new List[roomCount]; + for (var i = 0; i < roomCount; i++) adj[i] = []; + foreach (var (a, b) in mstEdges) + { + adj[a].Add(b); + adj[b].Add(a); + } + + // 5. Select start room: closest to grid center + var gridCx = width / 2f; + var gridCy = height / 2f; + var startRoom = 0; + var bestStartDist = float.MaxValue; + for (var i = 0; i < roomCount; i++) + { + var (rx, ry, rw, rh) = rooms[i]; + var dx = (rx + rw / 2f) - gridCx; + var dy = (ry + rh / 2f) - gridCy; + var d = dx * dx + dy * dy; + if (d < bestStartDist) { bestStartDist = d; startRoom = i; } + } + + // 6. Select end room: farthest from start via BFS graph distance + var bfsDist = new int[roomCount]; + Array.Fill(bfsDist, -1); + bfsDist[startRoom] = 0; + var bfsQueue = new Queue(); + bfsQueue.Enqueue(startRoom); + var endRoom = startRoom; + var maxDist = 0; + + while (bfsQueue.Count > 0) + { + var cur = bfsQueue.Dequeue(); + foreach (var nb in adj[cur]) + { + if (bfsDist[nb] >= 0) continue; + bfsDist[nb] = bfsDist[cur] + 1; + if (bfsDist[nb] > maxDist) { maxDist = bfsDist[nb]; endRoom = nb; } + bfsQueue.Enqueue(nb); + } + } + + // 7. Flood-fill connectivity check from start room — add corridors if any room unreachable + EnsureConnectivity(data, width, height, rooms, startRoom, corridorWidth); + + var terrain = new WalkabilitySnapshot { Width = width, Height = height, Data = data }; + + // Convert room centers to world positions + var g2w = 1f / worldToGrid; + var (sx, sy, sw, sh) = rooms[startRoom]; + var startWorld = new Vector2((sx + sw / 2f) * g2w, (sy + sh / 2f) * g2w); + var (ex, ey, ew, eh) = rooms[endRoom]; + var endWorld = new Vector2((ex + ew / 2f) * g2w, (ey + eh / 2f) * g2w); + + return new DungeonResult(terrain, startWorld, endWorld, rooms); + } + + private static void CarveCorridor(byte[] data, int width, int height, + int x1, int y1, int x2, int y2, int thickness) + { + var half = thickness / 2; + + if (y1 == y2) + { + // Horizontal + var minX = Math.Min(x1, x2); + var maxX = Math.Max(x1, x2); + for (var x = minX; x <= maxX; x++) + for (var dy = -half; dy <= half; dy++) + { + var y = y1 + dy; + if (x >= 0 && x < width && y >= 0 && y < height) + data[y * width + x] = 1; + } + } + else + { + // Vertical + var minY = Math.Min(y1, y2); + var maxY = Math.Max(y1, y2); + for (var y = minY; y <= maxY; y++) + for (var dx = -half; dx <= half; dx++) + { + var x = x1 + dx; + if (x >= 0 && x < width && y >= 0 && y < height) + data[y * width + x] = 1; + } + } + } + + private static void EnsureConnectivity(byte[] data, int width, int height, + List<(int x, int y, int w, int h)> rooms, int startRoom, int corridorWidth) + { + // Flood-fill from start room center + var (sx, sy, sw, sh) = rooms[startRoom]; + var seedX = sx + sw / 2; + var seedY = sy + sh / 2; + + var visited = new bool[width * height]; + var queue = new Queue<(int x, int y)>(); + queue.Enqueue((seedX, seedY)); + visited[seedY * width + seedX] = true; + + while (queue.Count > 0) + { + var (cx, cy) = queue.Dequeue(); + int[] dx = [-1, 1, 0, 0]; + int[] dy = [0, 0, -1, 1]; + for (var d = 0; d < 4; d++) + { + var nx = cx + dx[d]; + var ny = cy + dy[d]; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; + var idx = ny * width + nx; + if (visited[idx] || data[idx] == 0) continue; + visited[idx] = true; + queue.Enqueue((nx, ny)); + } + } + + // Check each room — if its center isn't reachable, carve a corridor to start + for (var i = 0; i < rooms.Count; i++) + { + if (i == startRoom) continue; + var (rx, ry, rw, rh) = rooms[i]; + var rcx = rx + rw / 2; + var rcy = ry + rh / 2; + if (visited[rcy * width + rcx]) continue; + + // Carve corridor from this room to start room + CarveCorridor(data, width, height, rcx, rcy, seedX, rcy, corridorWidth); + CarveCorridor(data, width, height, seedX, rcy, seedX, seedY, corridorWidth); + } + } + public static WalkabilitySnapshot Generate(int width, int height, int? seed = null) { var data = new byte[width * height]; diff --git a/src/Nexus.Systems/BotTick.cs b/src/Nexus.Systems/BotTick.cs index cc58374..53e5921 100644 --- a/src/Nexus.Systems/BotTick.cs +++ b/src/Nexus.Systems/BotTick.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Nexus.Core; using Nexus.Data; using Nexus.Pathfinding; @@ -18,6 +19,11 @@ public static class BotTick actionQueue.Clear(); movementBlender.Clear(); + + // Update stuck detection BEFORE systems run so IsStuck is current + if (state.Player.HasPosition) + movementBlender.UpdateStuckState(state.Player.Position); + nav.Update(state); foreach (var sys in systems) @@ -25,16 +31,58 @@ public static class BotTick sys.Update(state, actionQueue, movementBlender); // Wall repulsion — push away from nearby walls to prevent getting stuck + // Layer 5: never dropped by stuck detection (unlike L2 orbit) if (state.Terrain is { } terrain && state.Player.HasPosition) { var wallPush = TerrainQuery.ComputeWallRepulsion(terrain, state.Player.Position, config.WorldToGrid); if (wallPush.LengthSquared() > 0.0001f) - movementBlender.Submit(new MovementIntent(2, wallPush * 0.6f, 0.3f, "WallPush")); + { + // Boost wall push when stuck + var mag = movementBlender.IsStuck ? 1.0f : 0.6f; + var ovr = movementBlender.IsStuck ? 0.8f : 0.5f; + movementBlender.Submit(new MovementIntent(5, wallPush * mag, ovr, "WallPush")); + } } - if (nav.DesiredDirection.HasValue) + // Combat engagement: when visible enemies are nearby and we're not fleeing, + // navigate toward them instead of the exit. If fleeing, use normal nav to support escape. + var shouldEngage = false; + if (state.ThreatAssessment is not { ShouldFlee: true }) + { + var enemyCentroid = Vector2.Zero; + var enemyCount = 0; + foreach (var m in state.HostileMonsters) + { + if (!m.IsAlive || m.DistanceToPlayer >= config.CombatEngagementRange) continue; + if (state.Terrain is { } los && + !TerrainQuery.HasLineOfSight(los, state.Player.Position, m.Position, config.WorldToGrid)) + continue; + enemyCentroid += m.Position; + enemyCount++; + } + + if (enemyCount > 0 && state.Player.HasPosition) + { + shouldEngage = true; + enemyCentroid /= enemyCount; + var toEnemies = enemyCentroid - state.Player.Position; + if (toEnemies.LengthSquared() > 1f) + movementBlender.Submit(new MovementIntent(3, Vector2.Normalize(toEnemies), 0f, "Navigation")); + } + } + + if (!shouldEngage && nav.DesiredDirection.HasValue) movementBlender.Submit(new MovementIntent(3, nav.DesiredDirection.Value, 0f, "Navigation")); + // Predictive wall steering — look ahead along movement direction and steer around walls + if (!shouldEngage && state.Terrain is { } t2 && state.Player.HasPosition && nav.DesiredDirection.HasValue) + { + var steer = TerrainQuery.ComputeWallSteering(t2, state.Player.Position, + nav.DesiredDirection.Value, config.WorldToGrid); + if (steer.LengthSquared() > 0.0001f) + movementBlender.Submit(new MovementIntent(4, steer, 0.4f, "WallSteer")); + } + movementBlender.Resolve(state.Terrain, state.Player.Position, config.WorldToGrid); var resolved = actionQueue.Resolve(); diff --git a/src/Nexus.Systems/MovementSystem.cs b/src/Nexus.Systems/MovementSystem.cs index 6ffeb4c..3651368 100644 --- a/src/Nexus.Systems/MovementSystem.cs +++ b/src/Nexus.Systems/MovementSystem.cs @@ -22,7 +22,7 @@ public class MovementSystem : ISystem public float WorldToGrid { get; set; } = 23f / 250f; /// Minimum distance before radial push kicks in hard. - public float MinComfortDistance { get; set; } = 150f; + public float MinComfortDistance { get; set; } = 80f; private int _orbitSign = 1; @@ -33,7 +33,7 @@ public class MovementSystem : ISystem var playerPos = state.Player.Position; - // Compute weighted centroid and closest distance of nearby hostiles + // Compute weighted centroid and closest distance of nearby hostiles (LOS only) var centroid = Vector2.Zero; var count = 0; var closestDist = float.MaxValue; @@ -42,6 +42,9 @@ public class MovementSystem : ISystem { if (!monster.IsAlive) continue; if (monster.DistanceToPlayer > SafeDistance) continue; + if (state.Terrain is { } t && + !TerrainQuery.HasLineOfSight(t, playerPos, monster.Position, WorldToGrid)) + continue; centroid += monster.Position; count++; @@ -62,14 +65,14 @@ public class MovementSystem : ISystem var tangent = new Vector2(-centroidDir.Y, centroidDir.X) * _orbitSign; // Radial component — push away from centroid, strength based on proximity - // Close < MinComfort: strong push out - // MinComfort..SafeDistance*0.5: gentle push out + // Close < MinComfort: gentle push out (avoid stacking on top of enemies) + // MinComfort..SafeDistance*0.5: slight push out // SafeDistance*0.7+: pull inward to maintain engagement instead of drifting away float radialStrength; if (closestDist < MinComfortDistance) - radialStrength = -0.6f; // too close — push outward + radialStrength = -0.25f; // too close — gentle push outward else if (closestDist < SafeDistance * 0.5f) - radialStrength = -0.3f; // somewhat close — moderate push outward + radialStrength = -0.1f; // somewhat close — slight push outward else if (closestDist > SafeDistance * 0.7f) radialStrength = 0.4f; // at edge — pull inward to maintain engagement else @@ -81,8 +84,12 @@ public class MovementSystem : ISystem if (result.LengthSquared() < 0.0001f) return; // Override: attenuate navigation (layer 3) when actively orbiting enemies. - // Without this, navigation at full weight pulls the bot past enemies. - float orbitOverride = closestDist < SafeDistance * 0.7f ? 0.8f : 0.5f; + // Keep moderate so navigation can still guide past walls. + float orbitOverride = closestDist < SafeDistance * 0.7f ? 0.5f : 0.3f; + + // Suppress orbit when blender detects stuck — let navigation guide out + if (movement.IsStuck) + return; movement.Submit(new MovementIntent(2, Vector2.Normalize(result) * RepulsionWeight, orbitOverride, "Orbit")); } diff --git a/src/Nexus.Systems/ThreatSystem.cs b/src/Nexus.Systems/ThreatSystem.cs index 7dacac3..744333f 100644 --- a/src/Nexus.Systems/ThreatSystem.cs +++ b/src/Nexus.Systems/ThreatSystem.cs @@ -320,7 +320,7 @@ public class ThreatSystem : ISystem if (state.Terrain is { } terrain) { hasLos = TerrainQuery.HasLineOfSight(terrain, playerPos, monster.Position, WorldToGrid); - if (!hasLos) score *= 0.4f; + if (!hasLos) score *= 0.05f; } // Low HP monsters are less threatening From f09ee5d1062fc4fb0cc5718bbb5e601493f8f790 Mon Sep 17 00:00:00 2001 From: Boki Date: Sat, 4 Apr 2026 16:44:32 -0400 Subject: [PATCH 5/5] work on sim bot --- imgui.ini | 2 +- src/Nexus.Core/ActionExecutor.cs | 39 ++++- src/Nexus.Core/Actions.cs | 2 + src/Nexus.Core/Enums.cs | 1 + src/Nexus.Core/GameState.cs | 2 + src/Nexus.Core/HandModel.cs | 110 ++++++++++++ src/Nexus.Core/IInputController.cs | 8 + src/Nexus.Core/MovementBlender.cs | 12 +- src/Nexus.Core/MovementKeyTracker.cs | 5 + src/Nexus.Core/PlayerState.cs | 4 + src/Nexus.Core/ProjectileSnapshot.cs | 16 ++ src/Nexus.Pathfinding/NavigationController.cs | 45 +++++ .../Bridge/SimInputController.cs | 23 +++ src/Nexus.Simulator/Bridge/SimStateBuilder.cs | 112 ++++++++++++- src/Nexus.Simulator/Config/SimConfig.cs | 5 + src/Nexus.Simulator/Program.cs | 10 +- .../Rendering/EntityRenderer.cs | 52 +++++- .../Rendering/InputOverlayRenderer.cs | 17 +- src/Nexus.Simulator/Rendering/SimRenderer.cs | 36 +++- src/Nexus.Simulator/World/SimItem.cs | 15 ++ src/Nexus.Simulator/World/SimPlayer.cs | 20 +++ src/Nexus.Simulator/World/SimWorld.cs | 157 +++++++++++++++++- src/Nexus.Systems/AreaProgressionSystem.cs | 53 ++++-- src/Nexus.Systems/BotTick.cs | 20 ++- src/Nexus.Systems/DodgeSystem.cs | 93 +++++++++++ src/Nexus.Systems/LootSystem.cs | 35 +++- src/Nexus.Systems/MovementSystem.cs | 21 ++- src/Nexus.Systems/SystemFactory.cs | 1 + src/Nexus.Systems/ThreatSystem.cs | 33 ++-- 29 files changed, 889 insertions(+), 60 deletions(-) create mode 100644 src/Nexus.Core/HandModel.cs create mode 100644 src/Nexus.Core/ProjectileSnapshot.cs create mode 100644 src/Nexus.Simulator/World/SimItem.cs create mode 100644 src/Nexus.Systems/DodgeSystem.cs diff --git a/imgui.ini b/imgui.ini index 5af8267..9c98818 100644 --- a/imgui.ini +++ b/imgui.ini @@ -10,6 +10,6 @@ Collapsed=0 [Window][Simulator] Pos=564,96 -Size=893,571 +Size=1023,810 Collapsed=0 diff --git a/src/Nexus.Core/ActionExecutor.cs b/src/Nexus.Core/ActionExecutor.cs index 11b3ec6..0325b67 100644 --- a/src/Nexus.Core/ActionExecutor.cs +++ b/src/Nexus.Core/ActionExecutor.cs @@ -4,11 +4,26 @@ namespace Nexus.Core; public static class ActionExecutor { + // Screen center (half of 2560x1440) + private const float ScreenCenterX = 1280f; + private const float ScreenCenterY = 720f; + + // How far ahead of the player (in screen pixels) the idle cursor sits + private const float IdleCursorDistance = 200f; + public static void Execute(List resolved, IInputController input, - MovementKeyTracker moveTracker, MovementBlender blender, Vector2? playerPos = null) + MovementKeyTracker moveTracker, MovementBlender blender, Vector2? playerPos = null, + Matrix4x4? camera = null) { if (!input.IsInitialized) return; + var hasCast = false; + + // Filter out physically impossible key combos (same finger) + resolved = HandModel.Filter(resolved, + moveTracker.IsWHeld, moveTracker.IsAHeld, + moveTracker.IsSHeld, moveTracker.IsDHeld); + // Discrete actions foreach (var action in resolved) { @@ -19,6 +34,7 @@ public static class ActionExecutor break; case CastAction cast: + hasCast = true; if (cast.TargetScreenPos.HasValue) input.SmoothMoveTo((int)cast.TargetScreenPos.Value.X, (int)cast.TargetScreenPos.Value.Y); input.KeyPress(cast.SkillScanCode); @@ -43,6 +59,27 @@ public static class ActionExecutor case KeyActionType.Up: input.KeyUp(key.ScanCode); break; } break; + + case DodgeRollAction dodge: + input.SetDodgeDirection(dodge.Direction); + input.KeyPress(0x39); // Space bar + break; + } + } + + // Idle mouse tracking: when not casting, keep cursor ahead of player in movement direction. + // This prevents jarring jumps from target to target and gives smooth cursor flow. + if (!hasCast && blender.Direction is { } moveDir && camera.HasValue && playerPos.HasValue) + { + // Project a point slightly ahead of the player in the movement direction + var aheadWorld = playerPos.Value + moveDir * 300f; + var screenAhead = WorldToScreen.Project(aheadWorld, 0f, camera.Value); + if (screenAhead.HasValue) + { + // Clamp to reasonable screen bounds + var sx = Math.Clamp(screenAhead.Value.X, 100f, 2460f); + var sy = Math.Clamp(screenAhead.Value.Y, 100f, 1340f); + input.SmoothMoveTo((int)sx, (int)sy); } } diff --git a/src/Nexus.Core/Actions.cs b/src/Nexus.Core/Actions.cs index 453c5f5..35bf7d7 100644 --- a/src/Nexus.Core/Actions.cs +++ b/src/Nexus.Core/Actions.cs @@ -25,3 +25,5 @@ public record FlaskAction(int Priority, ushort FlaskScanCode) : BotAction(Priori public record ChatAction(int Priority, string Message) : BotAction(Priority); public record WaitAction(int Priority, int DurationMs) : BotAction(Priority); + +public record DodgeRollAction(int Priority, Vector2 Direction) : BotAction(Priority); diff --git a/src/Nexus.Core/Enums.cs b/src/Nexus.Core/Enums.cs index 1a41bea..57915a7 100644 --- a/src/Nexus.Core/Enums.cs +++ b/src/Nexus.Core/Enums.cs @@ -12,6 +12,7 @@ public enum DangerLevel public static class SystemPriority { public const int Threat = 50; + public const int Dodge = 75; public const int Movement = 100; public const int Navigation = 200; public const int Combat = 300; diff --git a/src/Nexus.Core/GameState.cs b/src/Nexus.Core/GameState.cs index e7125c0..f1de0b4 100644 --- a/src/Nexus.Core/GameState.cs +++ b/src/Nexus.Core/GameState.cs @@ -27,6 +27,8 @@ public class GameState /// In-progress quests from the quest linked list with target areas and paths. public IReadOnlyList Quests { get; set; } = []; + public IReadOnlyList EnemyProjectiles { get; set; } = []; + // Derived (computed once per tick by GameStateEnricher / ThreatSystem) public ThreatMap Threats { get; set; } = new(); public ThreatAssessment ThreatAssessment { get; set; } = new(); diff --git a/src/Nexus.Core/HandModel.cs b/src/Nexus.Core/HandModel.cs new file mode 100644 index 0000000..ed02730 --- /dev/null +++ b/src/Nexus.Core/HandModel.cs @@ -0,0 +1,110 @@ +namespace Nexus.Core; + +public enum Finger { Pinky, Ring, Middle, Index, Thumb } + +/// +/// Maps left-hand keys to physical fingers and filters out actions that would +/// require the same finger simultaneously. Dropped actions retry next tick (16ms). +/// Right hand (mouse) is unconstrained. +/// +public static class HandModel +{ + private static readonly Dictionary FingerMap = new() + { + // Pinky: 1, Q, A + [ScanCodes.Key1] = Finger.Pinky, + [ScanCodes.Q] = Finger.Pinky, + [ScanCodes.A] = Finger.Pinky, + + // Ring: 2, W, S + [ScanCodes.Key2] = Finger.Ring, + [ScanCodes.W] = Finger.Ring, + [ScanCodes.S] = Finger.Ring, + + // Middle: 3, E, D + [ScanCodes.Key3] = Finger.Middle, + [ScanCodes.E] = Finger.Middle, + [ScanCodes.D] = Finger.Middle, + + // Index: 4, 5, R, T, F + [ScanCodes.Key4] = Finger.Index, + [ScanCodes.Key5] = Finger.Index, + [ScanCodes.R] = Finger.Index, + [ScanCodes.T] = Finger.Index, + [ScanCodes.F] = Finger.Index, + + // Thumb: Space, LAlt + [ScanCodes.Space] = Finger.Thumb, + [ScanCodes.LAlt] = Finger.Thumb, + }; + + // Lower = higher priority when two actions compete for the same finger + private static int ActionTypePriority(BotAction a) => a switch + { + DodgeRollAction => 0, + FlaskAction => 1, + CastAction => 2, + KeyAction => 3, + _ => 4, + }; + + public static List Filter(List resolved, + bool wHeld, bool aHeld, bool sHeld, bool dHeld) + { + // Build occupied set from currently held WASD keys + var occupied = new HashSet(); + if (wHeld) occupied.Add(Finger.Ring); + if (aHeld) occupied.Add(Finger.Pinky); + if (sHeld) occupied.Add(Finger.Ring); + if (dHeld) occupied.Add(Finger.Middle); + + // Sort by action type priority (dodge > flask > cast > key) + resolved.Sort((a, b) => ActionTypePriority(a).CompareTo(ActionTypePriority(b))); + + var result = new List(resolved.Count); + + foreach (var action in resolved) + { + var scanCode = GetScanCode(action); + + // No scan code (ClickAction, ChatAction, WaitAction, MoveAction) → always pass + if (scanCode is null) + { + result.Add(action); + continue; + } + + // Key releases always pass — they free a finger + if (action is KeyAction { Type: KeyActionType.Up }) + { + result.Add(action); + continue; + } + + // No finger mapping for this scan code → pass (right-hand or unmapped key) + if (!FingerMap.TryGetValue(scanCode.Value, out var finger)) + { + result.Add(action); + continue; + } + + // Finger free → accept and mark occupied + if (occupied.Add(finger)) + { + result.Add(action); + } + // else: finger already occupied → drop, will retry next tick + } + + return result; + } + + private static ushort? GetScanCode(BotAction action) => action switch + { + DodgeRollAction => ScanCodes.Space, + FlaskAction f => f.FlaskScanCode, + CastAction c => c.SkillScanCode, + KeyAction k => k.ScanCode, + _ => null, + }; +} diff --git a/src/Nexus.Core/IInputController.cs b/src/Nexus.Core/IInputController.cs index 394e3c1..6b92156 100644 --- a/src/Nexus.Core/IInputController.cs +++ b/src/Nexus.Core/IInputController.cs @@ -1,3 +1,5 @@ +using System.Numerics; + namespace Nexus.Core; public interface IInputController @@ -16,4 +18,10 @@ public interface IInputController void LeftUp(); void RightDown(); void RightUp(); + + /// + /// Sets the direction for the next dodge roll. Called before KeyPress(0x21). + /// Default no-op for real input controllers (direction comes from game state). + /// + void SetDodgeDirection(Vector2 direction) { } } diff --git a/src/Nexus.Core/MovementBlender.cs b/src/Nexus.Core/MovementBlender.cs index 87bce56..d05a239 100644 --- a/src/Nexus.Core/MovementBlender.cs +++ b/src/Nexus.Core/MovementBlender.cs @@ -84,15 +84,19 @@ public sealed class MovementBlender if (IsStuck) { - // Keep only flee (L0, L1), navigation (L3), and wall push (L5) — drop orbit (L2) and herd (L4) + // Drop orbit (L2) and herd (L4) — they don't help when stuck _intents.RemoveAll(i => i.Layer == 2 || i.Layer == 4); - // After 1s stuck, inject a random perpendicular nudge to break free + // Drop flee (L0, L1) too — if we're stuck, flee is pointing into a wall. + // Let wall push and navigation guide us out instead. + _intents.RemoveAll(i => i.Layer <= 1); + + // After 750ms stuck, inject a random nudge at high priority to break free if (_stuckFrames > StuckRecoveryThreshold) { var angle = StuckRng.NextDouble() * Math.PI * 2; - var nudge = new System.Numerics.Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); - _intents.Add(new MovementIntent(1, nudge, 0.7f, "StuckEscape")); + var nudge = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); + _intents.Add(new MovementIntent(0, nudge, 0.6f, "StuckEscape")); // Reset counter so we try a new direction periodically if (_stuckFrames % 30 == 0) _stuckFrames = StuckRecoveryThreshold + 1; diff --git a/src/Nexus.Core/MovementKeyTracker.cs b/src/Nexus.Core/MovementKeyTracker.cs index 59204d1..9470e01 100644 --- a/src/Nexus.Core/MovementKeyTracker.cs +++ b/src/Nexus.Core/MovementKeyTracker.cs @@ -12,6 +12,11 @@ namespace Nexus.Core; public sealed class MovementKeyTracker { private bool _wHeld, _aHeld, _sHeld, _dHeld; + + public bool IsWHeld => _wHeld; + public bool IsAHeld => _aHeld; + public bool IsSHeld => _sHeld; + public bool IsDHeld => _dHeld; private long _wDownAt, _aDownAt, _sDownAt, _dDownAt; private int _wMinHold, _aMinHold, _sMinHold, _dMinHold; private long _wUpAt, _aUpAt, _sUpAt, _dUpAt; diff --git a/src/Nexus.Core/PlayerState.cs b/src/Nexus.Core/PlayerState.cs index bcd5f66..f9e02fa 100644 --- a/src/Nexus.Core/PlayerState.cs +++ b/src/Nexus.Core/PlayerState.cs @@ -31,4 +31,8 @@ public record PlayerState // Skill slots (populated by memory when available) public IReadOnlyList Skills { get; init; } = []; + + // Dodge roll state + public bool IsRolling { get; init; } + public float RollCooldownRemaining { get; init; } } diff --git a/src/Nexus.Core/ProjectileSnapshot.cs b/src/Nexus.Core/ProjectileSnapshot.cs new file mode 100644 index 0000000..91f549c --- /dev/null +++ b/src/Nexus.Core/ProjectileSnapshot.cs @@ -0,0 +1,16 @@ +using System.Numerics; + +namespace Nexus.Core; + +public record ProjectileSnapshot +{ + public Vector2 Position { get; init; } + public Vector2 Direction { get; init; } + public float Speed { get; init; } + public float HitRadius { get; init; } + public float DistanceToPlayer { get; init; } + /// Seconds until impact. Null if projectile will miss. + public float? TimeToImpact { get; init; } + /// Closest distance the projectile's trajectory passes to the player center. + public float ClosestApproachDistance { get; init; } +} diff --git a/src/Nexus.Pathfinding/NavigationController.cs b/src/Nexus.Pathfinding/NavigationController.cs index ea929d3..71fd322 100644 --- a/src/Nexus.Pathfinding/NavigationController.cs +++ b/src/Nexus.Pathfinding/NavigationController.cs @@ -42,6 +42,14 @@ public sealed class NavigationController // Grace period after picking a new explore target — don't check stuck immediately private int _stuckGraceTicks; + // Repeated stuck detection — force random walk after multiple failures at same spot + private int _repeatedStuckCount; + private Vector2 _lastStuckPos; + private const float RepeatedStuckRadius = 300f; // same-area detection + private const int RepeatedStuckLimit = 3; // failures before random walk + private int _randomWalkTicks; // countdown for forced random direction + private Vector2 _randomWalkDir; + public NavMode Mode { get; private set; } = NavMode.Idle; public Vector2? DesiredDirection { get; private set; } public IReadOnlyList? CurrentPath => _path; @@ -139,6 +147,8 @@ public sealed class NavigationController _exploreBiasPoint = null; _exploredGrid = null; _pathFailCooldownMs = 0; + _repeatedStuckCount = 0; + _randomWalkTicks = 0; IsExplorationComplete = false; } @@ -195,6 +205,20 @@ public sealed class NavigationController if (_stuckGraceTicks > 0) _stuckGraceTicks--; + // Random walk override — forced escape from repeated stuck loops + if (_randomWalkTicks > 0) + { + _randomWalkTicks--; + DesiredDirection = _randomWalkDir; + Status = "Random walk (escape)"; + if (_randomWalkTicks == 0) + { + _path = null; // force repath after random walk + _positionHistory.Clear(); + } + return; + } + var isStuck = false; if (_stuckGraceTicks <= 0 && _positionHistory.Count >= StuckWindowSize && _path is not null) { @@ -202,6 +226,27 @@ public sealed class NavigationController if (Vector2.Distance(oldest, playerPos) < StuckThreshold) { isStuck = true; + + // Track repeated stuck at the same location + if (Vector2.Distance(playerPos, _lastStuckPos) < RepeatedStuckRadius) + _repeatedStuckCount++; + else + _repeatedStuckCount = 1; + _lastStuckPos = playerPos; + + if (_repeatedStuckCount >= RepeatedStuckLimit) + { + // Force random walk to break free + var angle = (float)(_rng.NextDouble() * Math.PI * 2); + _randomWalkDir = new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + _randomWalkTicks = 120; // ~2 seconds of random walk + _repeatedStuckCount = 0; + Log.Information("NavigationController: repeated stuck at ({X:F0},{Y:F0}), forcing random walk", + playerPos.X, playerPos.Y); + DesiredDirection = _randomWalkDir; + return; + } + if (Mode == NavMode.Exploring) { Log.Information("NavigationController: stuck while exploring, picking new target"); diff --git a/src/Nexus.Simulator/Bridge/SimInputController.cs b/src/Nexus.Simulator/Bridge/SimInputController.cs index aed2596..6a1f9e6 100644 --- a/src/Nexus.Simulator/Bridge/SimInputController.cs +++ b/src/Nexus.Simulator/Bridge/SimInputController.cs @@ -28,6 +28,9 @@ public class SimInputController : IInputController private readonly float[] _mouseTimers = new float[3]; private const float FlashDuration = 0.3f; + // Dodge roll + private Vector2? _pendingDodgeDirection; + // Smooth mouse interpolation private Vector2 _mouseMoveStartPos; private Vector2 _mouseTargetPos; @@ -129,12 +132,32 @@ public class SimInputController : IInputController } } + public void SetDodgeDirection(Vector2 direction) + { + lock (_lock) { _pendingDodgeDirection = direction; } + } + public void KeyPress(ushort scanCode, int holdMs = 50) { lock (_lock) { _keyTimers[scanCode] = FlashDuration; } + + // Intercept dodge roll key (Space = 0x39) + if (scanCode == 0x39) + { + Vector2 dir; + lock (_lock) + { + dir = _pendingDodgeDirection ?? _world.MoveDirection; + _pendingDodgeDirection = null; + } + if (dir.LengthSquared() > 0.001f) + _world.QueueDodgeRoll(dir); + return; + } + // Queue as skill cast var target = ScreenToWorld(_mouseScreenPos); _world.QueueSkill(scanCode, target); diff --git a/src/Nexus.Simulator/Bridge/SimStateBuilder.cs b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs index e441879..27bc482 100644 --- a/src/Nexus.Simulator/Bridge/SimStateBuilder.cs +++ b/src/Nexus.Simulator/Bridge/SimStateBuilder.cs @@ -18,6 +18,7 @@ public static class SimStateBuilder var entities = new List(); var hostiles = new List(); + var nearbyLoot = new List(); foreach (var enemy in world.Enemies) { @@ -44,8 +45,65 @@ public static class SimStateBuilder hostiles.Add(snap); } + // Add area transition entity at dungeon exit so BotTick exit avoidance works + var exitSnap = new EntitySnapshot + { + Id = uint.MaxValue, + Path = "Metadata/Terrain/AreaTransition", + Category = EntityCategory.AreaTransition, + Position = world.EndWorldPos, + Z = 0f, + DistanceToPlayer = Vector2.Distance(world.EndWorldPos, player.Position), + IsAlive = false, + IsTargetable = true, + TransitionName = "DungeonExit", + }; + entities.Add(exitSnap); + + // Build loot snapshots + foreach (var item in world.Items) + { + var (rarity, isQuest) = item.Category switch + { + LootCategory.Magic => (MonsterRarity.Magic, false), + LootCategory.Rare => (MonsterRarity.Rare, false), + LootCategory.Unique => (MonsterRarity.Unique, false), + LootCategory.Quest => (MonsterRarity.White, true), + _ => (MonsterRarity.White, false), + }; + + var label = item.Category == LootCategory.Currency + ? $"Currency:{item.Label}" + : item.Label; + + var snap = new EntitySnapshot + { + Id = item.Id, + Path = "Metadata/MiscellaneousObjects/WorldItem", + Category = EntityCategory.WorldItem, + Rarity = rarity, + Position = item.Position, + Z = 0f, + DistanceToPlayer = Vector2.Distance(item.Position, player.Position), + IsAlive = true, + IsTargetable = true, + ItemBaseName = label, + IsQuestItem = isQuest, + }; + entities.Add(snap); + + // Only add filtered items (currency/rare/unique/quest) to NearbyLoot + if (item.Category is LootCategory.Currency or LootCategory.Rare + or LootCategory.Unique or LootCategory.Quest) + { + nearbyLoot.Add(snap); + } + } + var cameraMatrix = BuildCameraMatrix(player.Position); + var projectiles = BuildProjectileSnapshots(world, player.Position); + return new GameState { TickNumber = _tickNumber, @@ -60,7 +118,8 @@ public static class SimStateBuilder Terrain = world.Terrain, Entities = entities, HostileMonsters = hostiles, - NearbyLoot = [], + NearbyLoot = nearbyLoot, + EnemyProjectiles = projectiles, Player = new PlayerState { CharacterName = "SimPlayer", @@ -74,6 +133,8 @@ public static class SimStateBuilder EsCurrent = player.Es, EsTotal = player.MaxEs, Skills = BuildSkillStates(), + IsRolling = player.IsRolling, + RollCooldownRemaining = player.RollCooldownRemaining, }, }; } @@ -104,6 +165,55 @@ public static class SimStateBuilder -playerPos.X / halfW, -playerPos.Y / halfH, 0, 1); } + private const float PlayerRadius = 20f; + + private static List BuildProjectileSnapshots(SimWorld world, Vector2 playerPos) + { + var snapshots = new List(); + + foreach (var proj in world.Projectiles) + { + if (!proj.IsEnemyProjectile || proj.IsExpired) continue; + + var toPlayer = playerPos - proj.Position; + var dist = toPlayer.Length(); + + // Dot product: how far ahead of the projectile the player is (along travel direction) + var dot = Vector2.Dot(toPlayer, proj.Direction); + if (dot < 0) continue; // Moving away from player + + // Perpendicular distance from player center to projectile trajectory line + var closestDist = MathF.Abs(toPlayer.X * proj.Direction.Y - toPlayer.Y * proj.Direction.X); + + var collisionRadius = proj.HitRadius + PlayerRadius; + float? timeToImpact = null; + + if (closestDist < collisionRadius) + { + // Will hit — compute entry time via circle-line intersection + var discriminant = collisionRadius * collisionRadius - closestDist * closestDist; + var entryDist = dot - MathF.Sqrt(discriminant); + if (entryDist > 0) + timeToImpact = entryDist / proj.Speed; + else + timeToImpact = 0f; // Already overlapping + } + + snapshots.Add(new ProjectileSnapshot + { + Position = proj.Position, + Direction = proj.Direction, + Speed = proj.Speed, + HitRadius = proj.HitRadius, + DistanceToPlayer = dist, + TimeToImpact = timeToImpact, + ClosestApproachDistance = closestDist, + }); + } + + return snapshots; + } + private static List BuildSkillStates() { return diff --git a/src/Nexus.Simulator/Config/SimConfig.cs b/src/Nexus.Simulator/Config/SimConfig.cs index 4724343..7ca9c8a 100644 --- a/src/Nexus.Simulator/Config/SimConfig.cs +++ b/src/Nexus.Simulator/Config/SimConfig.cs @@ -54,6 +54,11 @@ public class SimConfig public int EnemyGroupMax { get; set; } = 18; public float EnemyGroupSpread { get; set; } = 120f; + // Dodge roll + public float DodgeRollDistance { get; set; } = 100f; // world units traveled per roll + public float DodgeRollDuration { get; set; } = 0.25f; // 250ms + public float DodgeRollCooldown { get; set; } = 1.0f; // 1s between rolls + // Player skills public float MeleeRange { get; set; } = 350f; public float MeleeConeAngle { get; set; } = 120f; diff --git a/src/Nexus.Simulator/Program.cs b/src/Nexus.Simulator/Program.cs index 76b8a01..52a7d23 100644 --- a/src/Nexus.Simulator/Program.cs +++ b/src/Nexus.Simulator/Program.cs @@ -72,8 +72,8 @@ foreach (var sys in systems) // ── Start simulation poller ── poller.Start(); -// ── Navigate to dungeon end ── -nav.NavigateTo(world.EndWorldPos); +// ── Explore the dungeon (not beeline to exit) ── +nav.Explore(); // ── Bot logic thread ── var actionQueue = new ActionQueue(); @@ -96,13 +96,13 @@ var botThread = new Thread(() => if (state is not null && !state.IsLoading && !state.IsEscapeOpen) { var resolved = BotTick.Run(state, systems, actionQueue, movementBlender, nav, botConfig); - ActionExecutor.Execute(resolved, input, moveTracker, movementBlender, state.Player.Position); + ActionExecutor.Execute(resolved, input, moveTracker, movementBlender, state.Player.Position, state.CameraMatrix); - // Check if dungeon end reached — regenerate and re-navigate + // Check if dungeon end reached — regenerate and explore new dungeon if (world.ReachedEnd) { world.RegenerateTerrain(); - nav.NavigateTo(world.EndWorldPos); + nav.Explore(); } botTickCount++; diff --git a/src/Nexus.Simulator/Rendering/EntityRenderer.cs b/src/Nexus.Simulator/Rendering/EntityRenderer.cs index b099953..1cd1ee5 100644 --- a/src/Nexus.Simulator/Rendering/EntityRenderer.cs +++ b/src/Nexus.Simulator/Rendering/EntityRenderer.cs @@ -12,8 +12,19 @@ public static class EntityRenderer var screenPos = vt.WorldToScreen(player.Position); var radius = 8f; - drawList.AddCircleFilled(screenPos, radius, 0xFF00FF00); // Green - drawList.AddCircle(screenPos, radius + 1, 0xFF00AA00); + if (player.IsRolling) + { + drawList.AddCircleFilled(screenPos, radius, 0xFFFFAA00); // Cyan when rolling + drawList.AddCircle(screenPos, radius + 1, 0xFFFFDD44); + // "R" centered in the circle + var textSize = ImGui.CalcTextSize("R"); + drawList.AddText(screenPos - textSize * 0.5f, 0xFF000000, "R"); + } + else + { + drawList.AddCircleFilled(screenPos, radius, 0xFF00FF00); // Green + drawList.AddCircle(screenPos, radius + 1, 0xFF00AA00); + } var barY = radius + 8; @@ -95,6 +106,43 @@ public static class EntityRenderer } } + public static void DrawItems(ImDrawListPtr drawList, IReadOnlyList items, + ViewTransform vt, Vector2 canvasMin, Vector2 canvasMax) + { + foreach (var item in items) + { + var screenPos = vt.WorldToScreen(item.Position); + + // Cull off-screen + if (screenPos.X < canvasMin.X - 60 || screenPos.X > canvasMax.X + 60 || + screenPos.Y < canvasMin.Y - 20 || screenPos.Y > canvasMax.Y + 20) + continue; + + var color = item.Category switch + { + LootCategory.Currency => 0xFF0000FF, // Red + LootCategory.Magic => 0xFFFF8800, // Blue + LootCategory.Rare => 0xFF00FFFF, // Yellow + LootCategory.Unique => 0xFF00AAFF, // Orange + LootCategory.Quest => 0xFF00FF00, // Green + _ => 0xFFCCCCCC, // White + }; + + var textSize = ImGui.CalcTextSize(item.Label); + var labelPos = screenPos - new Vector2(textSize.X * 0.5f, 16f); + + // Background + var pad = new Vector2(3, 1); + drawList.AddRectFilled(labelPos - pad, labelPos + textSize + pad, 0xBB000000); + + // Label text + drawList.AddText(labelPos, color, item.Label); + + // Small dot at item position + drawList.AddCircleFilled(screenPos, 3f, color); + } + } + private static void DrawHealthBar(ImDrawListPtr drawList, Vector2 pos, float width, float height, int current, int max, uint color) { diff --git a/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs b/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs index 6695c88..5a6a6ce 100644 --- a/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs +++ b/src/Nexus.Simulator/Rendering/InputOverlayRenderer.cs @@ -24,6 +24,7 @@ public static class InputOverlayRenderer private const uint ScrollBg = 0xFF333333; private const uint CursorDot = 0xFF00DDFF; // Cyan dot for cursor position private const uint CrosshairColor = 0x44FFFFFF; // Dim crosshair + private const float SpaceBarHeight = 20f; // Keyboard rows: (label, scanCode, column offset) private static readonly (string L, ushort S, float C)[] Row0 = @@ -44,7 +45,7 @@ public static class InputOverlayRenderer { var padSize = 80f; var mouseH = 64f; - var kbH = 3 * Stride; + var kbH = 3 * Stride + SpaceBarHeight + Gap; var totalH = kbH + 6 + mouseH + 6 + padSize; var origin = canvasOrigin + new Vector2(15, canvasSize.Y - totalH - 15); @@ -52,6 +53,7 @@ public static class InputOverlayRenderer DrawKeyRow(drawList, origin, Row0, 0, input); DrawKeyRow(drawList, origin, Row1, 1, input); DrawKeyRow(drawList, origin, Row2, 2, input); + DrawSpaceBar(drawList, origin + new Vector2(0, 3 * Stride), input); // Mouse to the right of keyboard var kbW = 4.25f * Stride + KeySize; @@ -79,6 +81,19 @@ public static class InputOverlayRenderer } } + private static void DrawSpaceBar(ImDrawListPtr drawList, Vector2 origin, InputSnapshot input) + { + const ushort spaceScan = 0x39; + var on = input.IsKeyActive(spaceScan); + + // Wide bar spanning roughly the ASDF row width + var pos = origin + new Vector2(0.5f * Stride, 0); + var size = new Vector2(3.5f * Stride, SpaceBarHeight); + + drawList.AddRectFilled(pos, pos + size, on ? ActiveBg : DarkBg, 3f); + drawList.AddRect(pos, pos + size, on ? Yellow : Outline, 3f); + } + private static void DrawMouse(ImDrawListPtr drawList, InputSnapshot input, Vector2 o) { const float w = 44, h = 64, hw = w / 2, bh = 26; diff --git a/src/Nexus.Simulator/Rendering/SimRenderer.cs b/src/Nexus.Simulator/Rendering/SimRenderer.cs index 0338ea6..541f242 100644 --- a/src/Nexus.Simulator/Rendering/SimRenderer.cs +++ b/src/Nexus.Simulator/Rendering/SimRenderer.cs @@ -75,17 +75,24 @@ public class SimRenderer var effects = _world.ActiveEffects.ToArray(); var projectiles = _world.Projectiles.ToArray(); var enemies = _world.Enemies.ToArray(); + var items = _world.Items.ToArray(); EffectRenderer.DrawEffects(drawList, effects, vt); EffectRenderer.DrawProjectiles(drawList, projectiles, vt); - // 4. Enemies + // 4. Items (ground loot labels) + EntityRenderer.DrawItems(drawList, items, vt, canvasOrigin, canvasOrigin + canvasSize); + + // 5. Enemies EntityRenderer.DrawEnemies(drawList, enemies, vt, canvasOrigin, canvasOrigin + canvasSize); - // 5. Player + // 6. Player EntityRenderer.DrawPlayer(drawList, _world.Player, vt); - // 6. Mock cursor — shows where the bot's mouse is pointing in the world + // 7. Monitor viewport outline — shows what a 2560x1440 monitor would see + DrawMonitorBounds(drawList, vt); + + // 8. Mock cursor — shows where the bot's mouse is pointing in the world DrawMockCursor(drawList, vt); drawList.PopClipRect(); @@ -139,6 +146,29 @@ public class SimRenderer } } + /// + /// Draws an axis-aligned rectangle centered on the player showing the approximate + /// field of view of a 2560x1440 monitor. Scales with sim zoom. + /// + private void DrawMonitorBounds(ImDrawListPtr drawList, ViewTransform vt) + { + // Approximate visible world area on a real monitor (~1400x788 world units at 16:9). + // Aggro range (600) sits comfortably inside. + const float halfW = 700f; + const float halfH = 394f; + + var center = vt.WorldToScreen(_world.Player.Position); + var scale = vt.WorldScale; + var sw = halfW * scale; + var sh = halfH * scale; + + var tl = center + new Vector2(-sw, -sh); + var br = center + new Vector2(sw, sh); + + const uint color = 0x44AAAAAA; + drawList.AddRect(tl, br, color); + } + private void DrawMockCursor(ImDrawListPtr drawList, ViewTransform vt) { // Convert the bot's mouse screen position to world, then to our viewport diff --git a/src/Nexus.Simulator/World/SimItem.cs b/src/Nexus.Simulator/World/SimItem.cs new file mode 100644 index 0000000..ceda3e4 --- /dev/null +++ b/src/Nexus.Simulator/World/SimItem.cs @@ -0,0 +1,15 @@ +using System.Numerics; + +namespace Nexus.Simulator.World; + +public enum LootCategory { Currency, Normal, Magic, Rare, Unique, Quest } + +public class SimItem +{ + private static uint _nextId = 50000; + + public uint Id { get; } = Interlocked.Increment(ref _nextId); + public Vector2 Position { get; init; } + public LootCategory Category { get; init; } + public string Label { get; init; } = ""; +} diff --git a/src/Nexus.Simulator/World/SimPlayer.cs b/src/Nexus.Simulator/World/SimPlayer.cs index 5b409e1..26947a1 100644 --- a/src/Nexus.Simulator/World/SimPlayer.cs +++ b/src/Nexus.Simulator/World/SimPlayer.cs @@ -17,6 +17,13 @@ public class SimPlayer public float EsRegen { get; set; } public float EsRechargeDelay { get; set; } + // Dodge roll state + public bool IsRolling { get; set; } + public Vector2 RollDirection { get; set; } + public float RollElapsed { get; set; } + public float RollDuration { get; set; } + public float RollCooldownRemaining { get; set; } + // Accumulate fractional regen private float _healthRegenAccum; private float _manaRegenAccum; @@ -41,8 +48,21 @@ public class SimPlayer _timeSinceLastDamage = esRechargeDelay; // Start with ES recharging } + public void StartDodgeRoll(Vector2 direction, float duration) + { + if (IsRolling || RollCooldownRemaining > 0) return; + IsRolling = true; + RollDirection = Vector2.Normalize(direction); + RollElapsed = 0f; + RollDuration = duration; + } + public void Update(float dt) { + // Tick dodge cooldown + if (RollCooldownRemaining > 0) + RollCooldownRemaining = MathF.Max(0, RollCooldownRemaining - dt); + _timeSinceLastDamage += dt; // Health regen (always active) diff --git a/src/Nexus.Simulator/World/SimWorld.cs b/src/Nexus.Simulator/World/SimWorld.cs index 681d295..b12ae1e 100644 --- a/src/Nexus.Simulator/World/SimWorld.cs +++ b/src/Nexus.Simulator/World/SimWorld.cs @@ -14,6 +14,7 @@ public class SimWorld public List Enemies { get; } = []; public List Projectiles { get; } = []; public List ActiveEffects { get; } = []; + public List Items { get; } = []; public WalkabilitySnapshot Terrain { get; private set; } public long TickNumber { get; private set; } public Vector2 StartWorldPos { get; private set; } @@ -30,6 +31,10 @@ public class SimWorld public Vector2 MouseWorldPos { get; set; } private readonly Queue<(ushort scanCode, Vector2 targetWorldPos)> _skillQueue = new(); + // Dodge roll + private Vector2? _pendingDodgeDirection; + private Vector2 _lastFacingDirection = Vector2.UnitX; + public SimWorld(SimConfig config) { _config = config; @@ -78,6 +83,7 @@ public class SimWorld Enemies.Clear(); Projectiles.Clear(); ActiveEffects.Clear(); + Items.Clear(); _respawnQueue.Clear(); SpawnEnemiesInRooms(dungeon.Rooms); Log.Information("Dungeon regenerated (seed={Seed}), {Rooms} rooms", _dungeonSeed, dungeon.Rooms.Count); @@ -88,6 +94,11 @@ public class SimWorld _skillQueue.Enqueue((scanCode, targetWorldPos)); } + public void QueueDodgeRoll(Vector2 direction) + { + _pendingDodgeDirection = direction; + } + public void Tick(float dt) { if (_config.IsPaused) return; @@ -102,6 +113,17 @@ public class SimWorld Log.Information("Reached dungeon end!"); } + // 0.5. Start queued dodge roll + if (_pendingDodgeDirection.HasValue && !Player.IsRolling && Player.RollCooldownRemaining <= 0) + { + Player.StartDodgeRoll(_pendingDodgeDirection.Value, _config.DodgeRollDuration); + _pendingDodgeDirection = null; + } + else + { + _pendingDodgeDirection = null; + } + // 1. Move player MovePlayer(dt); @@ -120,7 +142,10 @@ public class SimWorld // 6. Process respawn queue UpdateRespawns(dt); - // 7. Player regen + // 7. Pickup items near player + PickupNearbyItems(); + + // 8. Player regen Player.Update(dt); } @@ -134,9 +159,17 @@ public class SimWorld private void MovePlayer(float dt) { + // Dodge roll overrides normal movement + if (Player.IsRolling) + { + MovePlayerDodgeRoll(dt); + return; + } + if (MoveDirection.LengthSquared() < 0.001f) return; var dir = Vector2.Normalize(MoveDirection); + _lastFacingDirection = dir; // Track for stationary dodge var step = Player.MoveSpeed * dt; // Try full direction @@ -166,6 +199,46 @@ public class SimWorld } } + /// + /// Ease-out dodge roll: v(t) = (2D/T) * (1 - t/T). + /// Peak speed at start = 2D/T, decelerates to 0 over duration T. + /// Total distance traveled = D (integral of v over [0,T]). + /// + private void MovePlayerDodgeRoll(float dt) + { + Player.RollElapsed += dt; + var t = Player.RollElapsed; + var T = Player.RollDuration; + var D = _config.DodgeRollDistance; + + if (t >= T) + { + // Roll complete + Player.IsRolling = false; + Player.RollCooldownRemaining = _config.DodgeRollCooldown; + return; + } + + // Quadratic ease-out speed: peaks at 2D/T, drops to 0 + var speed = (2f * D / T) * (1f - t / T); + var step = speed * dt; + var dir = Player.RollDirection; + + // Try full direction, then wall-slide fallback + if (!TryMove(dir, step)) + { + // Try axis-aligned slides + if (MathF.Abs(dir.X) > 0.01f && TryMove(new Vector2(dir.X > 0 ? 1 : -1, 0), step)) { } + else if (MathF.Abs(dir.Y) > 0.01f && TryMove(new Vector2(0, dir.Y > 0 ? 1 : -1), step)) { } + else + { + // Completely blocked — end roll early + Player.IsRolling = false; + Player.RollCooldownRemaining = _config.DodgeRollCooldown; + } + } + } + private bool TryMove(Vector2 dir, float step) { var newPos = Player.Position + dir * step; @@ -246,7 +319,10 @@ public class SimWorld enemy.TakeDamage(_config.SkillBaseDamage); if (!enemy.IsAlive) + { Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); + SpawnLoot(enemy); + } } } @@ -269,7 +345,10 @@ public class SimWorld { enemy.TakeDamage(_config.SkillBaseDamage); if (!enemy.IsAlive) + { Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); + SpawnLoot(enemy); + } } } } @@ -337,7 +416,10 @@ public class SimWorld { enemy.TakeDamage(proj.Damage); if (!enemy.IsAlive) + { Log.Information("Kill: {Rarity} {Type} #{Id}", enemy.Rarity, enemy.Type, enemy.Id); + SpawnLoot(enemy); + } proj.IsExpired = true; break; } @@ -602,4 +684,77 @@ public class SimWorld if (roll < _config.UniqueChance + _config.RareChance + _config.MagicChance) return MonsterRarity.Magic; return MonsterRarity.White; } + + // --- Loot --- + + private void SpawnLoot(SimEnemy enemy) + { + var (dropChance, minItems, maxItems) = enemy.Rarity switch + { + MonsterRarity.Unique => (1f, 3, 5), + MonsterRarity.Rare => (0.8f, 2, 3), + MonsterRarity.Magic => (0.5f, 1, 2), + _ => (0.3f, 1, 1), + }; + + if (_rng.NextSingle() > dropChance) return; + + var count = _rng.Next(minItems, maxItems + 1); + for (var i = 0; i < count; i++) + { + var category = RollLootCategory(); + var offset = new Vector2( + (_rng.NextSingle() - 0.5f) * 60f, + (_rng.NextSingle() - 0.5f) * 60f); + + Items.Add(new SimItem + { + Position = enemy.Position + offset, + Category = category, + Label = PickLootName(category), + }); + } + } + + private LootCategory RollLootCategory() + { + var roll = _rng.NextSingle(); + if (roll < 0.15f) return LootCategory.Currency; + if (roll < 0.50f) return LootCategory.Normal; + if (roll < 0.75f) return LootCategory.Magic; + if (roll < 0.90f) return LootCategory.Rare; + if (roll < 0.95f) return LootCategory.Unique; + return LootCategory.Quest; + } + + private static readonly string[] CurrencyNames = ["Gold", "Scroll of Wisdom", "Exalted Orb", "Chaos Orb", "Transmutation Orb"]; + private static readonly string[] NormalNames = ["Iron Sword", "Leather Cap", "Bone Shield", "Short Bow", "Cloth Robe"]; + private static readonly string[] MagicNames = ["Runic Hatchet", "Serpent Wand", "Chain Gloves", "Wolf Pelt", "Jade Amulet"]; + private static readonly string[] RareNames = ["Dread Edge", "Soul Render", "Viper Strike", "Storm Circlet", "Blood Greaves"]; + private static readonly string[] UniqueNames = ["Headhunter", "Mageblood", "Ashes of the Stars", "Aegis Aurora"]; + private static readonly string[] QuestNames = ["Ancient Tablet", "Relic Shard", "Quest Gem", "Sealed Letter"]; + + private string PickLootName(LootCategory category) + { + var pool = category switch + { + LootCategory.Currency => CurrencyNames, + LootCategory.Normal => NormalNames, + LootCategory.Magic => MagicNames, + LootCategory.Rare => RareNames, + LootCategory.Unique => UniqueNames, + LootCategory.Quest => QuestNames, + _ => NormalNames, + }; + return pool[_rng.Next(pool.Length)]; + } + + private void PickupNearbyItems() + { + for (var i = Items.Count - 1; i >= 0; i--) + { + if (Vector2.Distance(Items[i].Position, Player.Position) < 80f) + Items.RemoveAt(i); + } + } } diff --git a/src/Nexus.Systems/AreaProgressionSystem.cs b/src/Nexus.Systems/AreaProgressionSystem.cs index 5295890..933c365 100644 --- a/src/Nexus.Systems/AreaProgressionSystem.cs +++ b/src/Nexus.Systems/AreaProgressionSystem.cs @@ -182,19 +182,12 @@ public sealed class AreaProgressionSystem : ISystem private void UpdateExploring(GameState state, ActionQueue actions) { - // ── Check 1: Yield for elite combat ── - const float EliteEngagementRange = 800f; - foreach (var m in state.HostileMonsters) + // ── Check 1: Yield for combat — stop exploring to clear nearby monsters ── + if (HasNearbyHostiles(state)) { - if (m.Rarity >= MonsterRarity.Rare && m.DistanceToPlayer < EliteEngagementRange) - { - if (_nav.Mode != NavMode.Idle) - { - Log.Information("Progression: yielding for {Rarity} (dist={Dist:F0})", m.Rarity, m.DistanceToPlayer); - _nav.Stop(); - } - return; - } + if (_nav.Mode != NavMode.Idle) + _nav.Stop(); + return; } // ── Check 2: Quest chest interaction ── @@ -462,6 +455,16 @@ public sealed class AreaProgressionSystem : ISystem private void UpdateNavigatingToTransition(GameState state, ActionQueue actions) { + // Hostiles nearby — abort transition, go fight + if (HasNearbyHostiles(state)) + { + Log.Debug("Progression: hostiles near transition, aborting to fight"); + _targetTransitionEntityId = 0; + _phase = Phase.Exploring; + _nav.Stop(); + return; + } + // Check if the entity is still visible and close enough foreach (var e in state.Entities) { @@ -488,6 +491,15 @@ public sealed class AreaProgressionSystem : ISystem private void UpdateInteracting(GameState state, ActionQueue actions) { + // Hostiles nearby — abort transition, go fight + if (HasNearbyHostiles(state)) + { + Log.Debug("Progression: hostiles near exit, aborting click to fight"); + _targetTransitionEntityId = 0; + _phase = Phase.Exploring; + return; + } + // Project entity to screen and click foreach (var e in state.Entities) { @@ -546,6 +558,23 @@ public sealed class AreaProgressionSystem : ISystem return best.Area.Id; } + private bool HasNearbyHostiles(GameState state) + { + const float EliteEngagementRange = 800f; + var combatRange = _config.CombatEngagementRange; + foreach (var m in state.HostileMonsters) + { + if (!m.IsAlive) continue; + var range = m.Rarity >= MonsterRarity.Rare ? EliteEngagementRange : combatRange; + if (m.DistanceToPlayer >= range) continue; + if (state.Terrain is { } t && + !TerrainQuery.HasLineOfSight(t, state.Player.Position, m.Position, _config.WorldToGrid)) + continue; + return true; + } + return false; + } + private bool HasQuestInThisArea(GameState state) { return state.Quests.Any(q => q.TargetAreas?.Any(a => diff --git a/src/Nexus.Systems/BotTick.cs b/src/Nexus.Systems/BotTick.cs index 53e5921..78f8de2 100644 --- a/src/Nexus.Systems/BotTick.cs +++ b/src/Nexus.Systems/BotTick.cs @@ -83,10 +83,28 @@ public static class BotTick movementBlender.Submit(new MovementIntent(4, steer, 0.4f, "WallSteer")); } + // Exit avoidance: when fighting near area transitions, push away to prevent + // accidentally walking into them (they auto-trigger on proximity) + const float ExitAvoidRange = 300f; + if (shouldEngage && state.Player.HasPosition) + { + foreach (var e in state.Entities) + { + if (e.Category != EntityCategory.AreaTransition) continue; + var away = state.Player.Position - e.Position; + var dist = away.Length(); + if (dist >= ExitAvoidRange || dist < 0.1f) continue; + var strength = 1.0f - (dist / ExitAvoidRange); + movementBlender.Submit(new MovementIntent(6, Vector2.Normalize(away) * strength, 0.9f, "ExitAvoid")); + break; + } + } + movementBlender.Resolve(state.Terrain, state.Player.Position, config.WorldToGrid); var resolved = actionQueue.Resolve(); - if (movementBlender.IsUrgentFlee) + // Block casting during emergency flee or dodge roll — focus on escaping + if (state.ThreatAssessment is { AnyEmergency: true } || state.Player.IsRolling) resolved.RemoveAll(a => a is CastAction); return resolved; diff --git a/src/Nexus.Systems/DodgeSystem.cs b/src/Nexus.Systems/DodgeSystem.cs new file mode 100644 index 0000000..6bc781c --- /dev/null +++ b/src/Nexus.Systems/DodgeSystem.cs @@ -0,0 +1,93 @@ +using System.Numerics; +using Nexus.Core; + +namespace Nexus.Systems; + +/// +/// Detects threatening enemy projectiles and triggers dodge rolls perpendicular to their trajectory. +/// Priority 75 — between Threat (50) and Movement (100). +/// +public class DodgeSystem : ISystem +{ + public int Priority => SystemPriority.Dodge; + public string Name => "Dodge"; + public bool IsEnabled { get; set; } = true; + + public float WorldToGrid { get; set; } = 23f / 250f; + + // Reaction window — only dodge projectiles arriving within this time + private const float ReactionWindow = 0.4f; + + // Only dodge projectiles whose closest approach is within this distance + private const float DodgeThreshold = 80f; + + // Minimum time between dodge decisions (prevents flip-flopping when multiple projectiles arrive) + private float _decisionCooldown; + private const float DecisionCooldownDuration = 0.1f; + + public void Update(GameState state, ActionQueue actions, MovementBlender movement) + { + var dt = state.DeltaTime; + if (_decisionCooldown > 0) + { + _decisionCooldown -= dt; + return; + } + + // Skip if already rolling, on cooldown, or no projectiles + if (state.Player.IsRolling) return; + if (state.Player.RollCooldownRemaining > 0) return; + if (state.EnemyProjectiles.Count == 0) return; + if (!state.Player.HasPosition) return; + + // Find most urgent threatening projectile + ProjectileSnapshot? urgent = null; + foreach (var proj in state.EnemyProjectiles) + { + if (proj.TimeToImpact is not { } tti) continue; // Will miss + if (tti > ReactionWindow) continue; // Too far away to react + if (proj.ClosestApproachDistance > DodgeThreshold) continue; + + if (urgent is null || tti < urgent.TimeToImpact) + urgent = proj; + } + + if (urgent is null) return; + + // Compute dodge direction: perpendicular to projectile trajectory + var projDir = urgent.Direction; + var perp1 = new Vector2(-projDir.Y, projDir.X); // Left perpendicular + var perp2 = new Vector2(projDir.Y, -projDir.X); // Right perpendicular + + // Choose the side that moves us AWAY from the projectile trajectory + var toPlayer = state.Player.Position - urgent.Position; + var dodgeDir = Vector2.Dot(toPlayer, perp1) > 0 ? perp1 : perp2; + + // Terrain validation: check if dodge destination is walkable + var dodgeDist = 100f; // approximate roll distance + var playerPos = state.Player.Position; + var dest1 = playerPos + dodgeDir * dodgeDist; + var gx1 = (int)(dest1.X * WorldToGrid); + var gy1 = (int)(dest1.Y * WorldToGrid); + + if (state.Terrain is { } terrain && !terrain.IsWalkable(gx1, gy1)) + { + // Try the other perpendicular + dodgeDir = -dodgeDir; + var dest2 = playerPos + dodgeDir * dodgeDist; + var gx2 = (int)(dest2.X * WorldToGrid); + var gy2 = (int)(dest2.Y * WorldToGrid); + + if (!terrain.IsWalkable(gx2, gy2)) + return; // Both sides blocked — can't dodge + } + + // Submit dodge action + actions.Submit(new DodgeRollAction(SystemPriority.Dodge, dodgeDir)); + + // Also submit movement bias so other systems know we want to go this way + movement.Submit(new MovementIntent(1, dodgeDir, 0.8f, "Dodge")); + + _decisionCooldown = DecisionCooldownDuration; + } +} diff --git a/src/Nexus.Systems/LootSystem.cs b/src/Nexus.Systems/LootSystem.cs index ffa67f4..0e4ec9f 100644 --- a/src/Nexus.Systems/LootSystem.cs +++ b/src/Nexus.Systems/LootSystem.cs @@ -1,3 +1,4 @@ +using System.Numerics; using Nexus.Core; namespace Nexus.Systems; @@ -6,10 +7,40 @@ public class LootSystem : ISystem { public int Priority => SystemPriority.Loot; public string Name => "Loot"; - public bool IsEnabled { get; set; } = false; + public bool IsEnabled { get; set; } = true; + + private const float LootRange = 400f; + private const float SafeRange = 600f; public void Update(GameState state, ActionQueue actions, MovementBlender movement) { - // STUB: loot detection and pickup logic + if (state.NearbyLoot.Count == 0) return; + + // Don't loot if hostiles are nearby + foreach (var hostile in state.HostileMonsters) + { + if (hostile.IsAlive && hostile.DistanceToPlayer < SafeRange) + return; + } + + // Find nearest loot within range + EntitySnapshot? nearest = null; + var nearestDist = float.MaxValue; + + foreach (var loot in state.NearbyLoot) + { + if (loot.DistanceToPlayer < nearestDist && loot.DistanceToPlayer < LootRange) + { + nearest = loot; + nearestDist = loot.DistanceToPlayer; + } + } + + if (nearest is null) return; + + // Steer toward the item — L3 same as navigation so it replaces explore direction + var dir = nearest.Position - state.Player.Position; + if (dir.LengthSquared() > 1f) + movement.Submit(new MovementIntent(3, Vector2.Normalize(dir), 0f, "Loot")); } } diff --git a/src/Nexus.Systems/MovementSystem.cs b/src/Nexus.Systems/MovementSystem.cs index 3651368..f0048a4 100644 --- a/src/Nexus.Systems/MovementSystem.cs +++ b/src/Nexus.Systems/MovementSystem.cs @@ -22,7 +22,7 @@ public class MovementSystem : ISystem public float WorldToGrid { get; set; } = 23f / 250f; /// Minimum distance before radial push kicks in hard. - public float MinComfortDistance { get; set; } = 80f; + public float MinComfortDistance { get; set; } = 150f; private int _orbitSign = 1; @@ -65,16 +65,19 @@ public class MovementSystem : ISystem var tangent = new Vector2(-centroidDir.Y, centroidDir.X) * _orbitSign; // Radial component — push away from centroid, strength based on proximity - // Close < MinComfort: gentle push out (avoid stacking on top of enemies) - // MinComfort..SafeDistance*0.5: slight push out - // SafeDistance*0.7+: pull inward to maintain engagement instead of drifting away + // Close < MinComfort: strong push out (don't let enemies stack on us) + // MinComfort..SafeDistance*0.6: moderate push out (keep distance) + // SafeDistance*0.6..0.8: pure orbit (sweet spot) + // SafeDistance*0.8+: gentle pull inward to maintain engagement float radialStrength; if (closestDist < MinComfortDistance) - radialStrength = -0.25f; // too close — gentle push outward - else if (closestDist < SafeDistance * 0.5f) - radialStrength = -0.1f; // somewhat close — slight push outward - else if (closestDist > SafeDistance * 0.7f) - radialStrength = 0.4f; // at edge — pull inward to maintain engagement + radialStrength = -0.6f; // too close — strong push outward + else if (closestDist < SafeDistance * 0.4f) + radialStrength = -0.35f; // close — firm push outward + else if (closestDist < SafeDistance * 0.6f) + radialStrength = -0.15f; // moderate — gentle push outward + else if (closestDist > SafeDistance * 0.8f) + radialStrength = 0.3f; // at edge — pull inward to maintain engagement else radialStrength = 0f; // sweet spot — pure orbit diff --git a/src/Nexus.Systems/SystemFactory.cs b/src/Nexus.Systems/SystemFactory.cs index 27aa401..47dddfe 100644 --- a/src/Nexus.Systems/SystemFactory.cs +++ b/src/Nexus.Systems/SystemFactory.cs @@ -15,6 +15,7 @@ public static class SystemFactory systems.Add(new AreaProgressionSystem(config, nav, AreaGraph.Load())); systems.Add(new ThreatSystem { WorldToGrid = config.WorldToGrid }); + systems.Add(new DodgeSystem { WorldToGrid = config.WorldToGrid }); systems.Add(new MovementSystem { SafeDistance = config.SafeDistance, diff --git a/src/Nexus.Systems/ThreatSystem.cs b/src/Nexus.Systems/ThreatSystem.cs index 744333f..bb8188f 100644 --- a/src/Nexus.Systems/ThreatSystem.cs +++ b/src/Nexus.Systems/ThreatSystem.cs @@ -43,6 +43,10 @@ public class ThreatSystem : ISystem private int _killLoseStreak; private const int KillTargetDebounce = 15; // ~250ms + // Flee commitment — once flee starts, hold for minimum duration to prevent oscillation + private int _fleeCommitTicks; + private const int FleeCommitDuration = 60; // ~1 second at 60Hz + // Logging private ThreatCategory _prevMaxCategory = ThreatCategory.Ignore; private uint? _prevTopThreatId; @@ -181,7 +185,14 @@ public class ThreatSystem : ISystem // Hysteresis on flee transition — require score to drop 15% below threshold to de-escalate var wasFleeing = _prevMaxCategory >= ThreatCategory.Flee; var fleeOffThreshold = wasFleeing ? FleeThreshold * 0.85f : FleeThreshold; - var shouldFlee = _smoothedZoneThreat > fleeOffThreshold || anyEmergency; + var rawShouldFlee = _smoothedZoneThreat > fleeOffThreshold || anyEmergency; + + // Flee commitment: once triggered, hold for minimum duration to prevent oscillation + if (rawShouldFlee) + _fleeCommitTicks = FleeCommitDuration; + else if (_fleeCommitTicks > 0) + _fleeCommitTicks--; + var shouldFlee = _fleeCommitTicks > 0; var areaClear = entries.TrueForAll(e => e.Category < ThreatCategory.Monitor); // Range band counts (backward compat) @@ -274,22 +285,10 @@ public class ThreatSystem : ISystem _prevMaxCategory = zoneCat; } - // ── 6. Submit movement intents ── - if (!shouldFlee) return; - - var isPointBlank = closestDist < PointBlankRange; - - if (anyEmergency || isPointBlank) - { - // Layer 0: near-total override — flee, blocks casting. 0.85 lets wall push still help. - movement.Submit(new MovementIntent(0, safest, 0.85f, "Threat")); - } - else - { - // Layer 1: strong flee scaled by flee weight - var override1 = 0.3f + assessment.FleeWeight * 0.4f; // 0.3–0.7 - movement.Submit(new MovementIntent(1, safest * assessment.FleeWeight, override1, "Threat")); - } + // ── 6. Movement ── + // No raw flee intents — all movement goes through pathfinding (NavigationController) + // which routes around walls. ThreatAssessment.ShouldFlee/SafestDirection are available + // for BotTick to adjust navigation target if needed. } // ── Per-entity scoring ──