From 77a842f8846db59a8ed31b45c140d52eaa48ef5f Mon Sep 17 00:00:00 2001 From: mlangkabel Date: Mon, 3 Aug 2026 21:13:24 +0200 Subject: [PATCH] dedupe AttackExecutor and RepairExecutor via executeOrbitAndAssign --- src/lib/ecs/system/CMakeLists.txt | 1 + src/lib/ecs/system/ai/AttackExecutor.cpp | 60 +------------ .../ecs/system/ai/OrbitAndAssignExecutor.h | 89 +++++++++++++++++++ src/lib/ecs/system/ai/RepairExecutor.cpp | 60 +------------ 4 files changed, 98 insertions(+), 112 deletions(-) create mode 100644 src/lib/ecs/system/ai/OrbitAndAssignExecutor.h diff --git a/src/lib/ecs/system/CMakeLists.txt b/src/lib/ecs/system/CMakeLists.txt index ed99ca3..5c91cff 100644 --- a/src/lib/ecs/system/CMakeLists.txt +++ b/src/lib/ecs/system/CMakeLists.txt @@ -8,6 +8,7 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/ai/Centroid.h ${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h ${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h + ${CMAKE_CURRENT_SOURCE_DIR}/ai/OrbitAndAssignExecutor.h ${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h ${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h ${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h diff --git a/src/lib/ecs/system/ai/AttackExecutor.cpp b/src/lib/ecs/system/ai/AttackExecutor.cpp index 9b434b3..ee7be2b 100644 --- a/src/lib/ecs/system/ai/AttackExecutor.cpp +++ b/src/lib/ecs/system/ai/AttackExecutor.cpp @@ -2,12 +2,8 @@ #include "AttackBehavior.h" #include "BehaviorKind.h" -#include "DynamicBodyComponent.h" #include "EntityAdmin.h" -#include "ModuleOwnerComponent.h" -#include "MovementIntentComponent.h" -#include "PositionComponent.h" -#include "SelectedBehaviorComponent.h" +#include "OrbitAndAssignExecutor.h" #include "tracing.h" #include "WeaponComponent.h" @@ -15,55 +11,7 @@ void AttackExecutor::execute(EntityAdmin& admin) { TRACE(); - // Ships: move toward the behavior target. - admin.forEach( - [&](entt::entity /*e*/, const AttackBehavior& attack, - const SelectedBehaviorComponent& selected, const PositionComponent& pos, - MovementIntentComponent& intent) - { - if (selected.winner != BehaviorKind::Attack) { return; } - if (!attack.currentTarget) { return; } - - const entt::entity t = *attack.currentTarget; - QVector2D center = pos.value; - float radius = 0.0f; - QVector2D centerVelocity; - if (admin.isValid(t) && admin.hasAll(t)) - { - center = admin.get(t).value; - radius = attack.orbitRadius_tiles; - if (admin.hasAll(t)) - { - centerVelocity = admin.get(t).velocity_tpt; - } - } - intent = MovementIntentComponent{true, center, radius, centerVelocity}; - }); - - // Weapons: assign the behavior target only if it is within this weapon's range. - admin.forEach( - [&](entt::entity /*we*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner) - { - if (!admin.hasAll(owner.owner)) - { - return; - } - const SelectedBehaviorComponent& selected = - admin.get(owner.owner); - if (selected.winner != BehaviorKind::Attack) { return; } - - const AttackBehavior& attack = admin.get(owner.owner); - if (!attack.currentTarget) { return; } - - const entt::entity t = *attack.currentTarget; - if (!admin.isValid(t) || !admin.hasAll(t)) { return; } - - const QVector2D ownerPos = admin.get(owner.owner).value; - const float dist = (admin.get(t).value - ownerPos).length(); - if (dist <= weapon.range_tiles) - { - weapon.currentTarget = t; - } - }); + // Orbit the attack target and hand it to every weapon that can reach it + // (REQ-SHP-ORBIT). + executeOrbitAndAssign(admin, BehaviorKind::Attack); } diff --git a/src/lib/ecs/system/ai/OrbitAndAssignExecutor.h b/src/lib/ecs/system/ai/OrbitAndAssignExecutor.h new file mode 100644 index 0000000..62eb848 --- /dev/null +++ b/src/lib/ecs/system/ai/OrbitAndAssignExecutor.h @@ -0,0 +1,89 @@ +#pragma once + +#include + +#include "entt/entity/entity.hpp" + +#include "BehaviorKind.h" +#include "DynamicBodyComponent.h" +#include "EntityAdmin.h" +#include "ModuleOwnerComponent.h" +#include "MovementIntentComponent.h" +#include "PositionComponent.h" +#include "SelectedBehaviorComponent.h" + +// Shared executor body for the behaviors that orbit a single target entity and then +// hand that target to the ship's in-range modules (REQ-SHP-ORBIT): Attack (with +// WeaponComponent) and Repair (with RepairToolComponent). +// +// Two passes, in this order — the order and the exact sequence of component writes +// are load-bearing for determinism (see the Tick Order section of +// docs/architecture.md): +// 1. Ships that have `Behavior` and won with `kind` write their MovementIntent to +// orbit the behavior's target at the behavior's orbit radius. A target that is +// gone (or has no position) degenerates to "hold position": the ship's own +// position with a zero radius. +// 2. Modules of type `ModuleComponent` whose owner won with `kind` adopt the +// behavior's target, but only when it lies within that module's own range. +// Out-of-range modules keep whatever target they already had, which +// CombatSystem/RepairSystem re-validate. +// +// `Behavior` must expose `std::optional currentTarget` and +// `float orbitRadius_tiles`; `ModuleComponent` must expose `float range_tiles` and +// `std::optional currentTarget`. +template +void executeOrbitAndAssign(EntityAdmin& admin, BehaviorKind kind) +{ + // Ships: move toward the behavior target. + admin.forEach( + [&](entt::entity /*e*/, const Behavior& behavior, + const SelectedBehaviorComponent& selected, const PositionComponent& pos, + MovementIntentComponent& intent) + { + if (selected.winner != kind) { return; } + if (!behavior.currentTarget) { return; } + + const entt::entity t = *behavior.currentTarget; + QVector2D center = pos.value; + float radius = 0.0f; + QVector2D centerVelocity; + if (admin.isValid(t) && admin.hasAll(t)) + { + center = admin.get(t).value; + radius = behavior.orbitRadius_tiles; + if (admin.hasAll(t)) + { + centerVelocity = admin.get(t).velocity_tpt; + } + } + intent = MovementIntentComponent{true, center, radius, centerVelocity}; + }); + + // Modules: assign the behavior target only if it is within this module's range. + admin.forEach( + [&](entt::entity /*me*/, ModuleComponent& module, + const ModuleOwnerComponent& owner) + { + if (!admin.hasAll(owner.owner)) + { + return; + } + const SelectedBehaviorComponent& selected = + admin.get(owner.owner); + if (selected.winner != kind) { return; } + + const Behavior& behavior = admin.get(owner.owner); + if (!behavior.currentTarget) { return; } + + const entt::entity t = *behavior.currentTarget; + if (!admin.isValid(t) || !admin.hasAll(t)) { return; } + + const QVector2D ownerPos = admin.get(owner.owner).value; + const float dist = (admin.get(t).value - ownerPos).length(); + if (dist <= module.range_tiles) + { + module.currentTarget = t; + } + }); +} diff --git a/src/lib/ecs/system/ai/RepairExecutor.cpp b/src/lib/ecs/system/ai/RepairExecutor.cpp index 44af5ad..0809a2d 100644 --- a/src/lib/ecs/system/ai/RepairExecutor.cpp +++ b/src/lib/ecs/system/ai/RepairExecutor.cpp @@ -1,69 +1,17 @@ #include "RepairExecutor.h" #include "BehaviorKind.h" -#include "DynamicBodyComponent.h" #include "EntityAdmin.h" -#include "ModuleOwnerComponent.h" -#include "MovementIntentComponent.h" -#include "PositionComponent.h" +#include "OrbitAndAssignExecutor.h" #include "RepairBehavior.h" #include "RepairToolComponent.h" -#include "SelectedBehaviorComponent.h" #include "tracing.h" void RepairExecutor::execute(EntityAdmin& admin) { TRACE(); - // Ships: move toward the repair target. - admin.forEach( - [&](entt::entity /*e*/, const RepairBehavior& repair, - const SelectedBehaviorComponent& selected, const PositionComponent& pos, - MovementIntentComponent& intent) - { - if (selected.winner != BehaviorKind::Repair) { return; } - if (!repair.currentTarget) { return; } - - const entt::entity t = *repair.currentTarget; - QVector2D center = pos.value; - float radius = 0.0f; - QVector2D centerVelocity; - if (admin.isValid(t) && admin.hasAll(t)) - { - center = admin.get(t).value; - radius = repair.orbitRadius_tiles; - if (admin.hasAll(t)) - { - centerVelocity = admin.get(t).velocity_tpt; - } - } - intent = MovementIntentComponent{true, center, radius, centerVelocity}; - }); - - // Repair tools: prefer the behavior target if it is within tool range. - admin.forEach( - [&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner) - { - if (!admin.hasAll(owner.owner)) - { - return; - } - const SelectedBehaviorComponent& selected = - admin.get(owner.owner); - if (selected.winner != BehaviorKind::Repair) { return; } - - const RepairBehavior& repair = admin.get(owner.owner); - if (!repair.currentTarget) { return; } - - const entt::entity t = *repair.currentTarget; - if (!admin.isValid(t) || !admin.hasAll(t)) { return; } - - const QVector2D ownerPos = admin.get(owner.owner).value; - const float dist = (admin.get(t).value - ownerPos).length(); - if (dist <= tool.range_tiles) - { - tool.currentTarget = t; - } - }); + // Orbit the repair target and hand it to every repair tool that can reach it + // (REQ-SHP-ORBIT). + executeOrbitAndAssign(admin, BehaviorKind::Repair); }