make ships orbit their targets

This commit is contained in:
2026-06-15 21:09:40 +02:00
parent 6b7c3df64a
commit 4153b7e2f5
17 changed files with 209 additions and 20 deletions

View File

@@ -268,6 +268,8 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));

View File

@@ -49,6 +49,8 @@ struct WorldConfig
double beltSpeed_tps; // REQ-GW-BELT-SPEED (tiles/s, converted from m/s in config)
int tunnelMaxDistance_tiles; // REQ-BLD-TUNNEL-PAIR
double departureIntervalSeconds; // REQ-SHP-RALLY
double orbitFactor; // REQ-SHP-ORBIT (multiplies tool range for orbit radius)
double rallyOrbitRadius_tiles; // REQ-SHP-ORBIT (fixed orbit radius around the rally point)
WorldRegions regions;
WorldExpansion expansion;

View File

@@ -9,5 +9,6 @@
struct AttackBehavior
{
std::optional<entt::entity> currentTarget;
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
float score = 0.0f;
};

View File

@@ -7,5 +7,6 @@
struct RallyBehavior
{
QVector2D rallyPoint;
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
float score = 0.0f;
};

View File

@@ -11,5 +11,6 @@ struct RepairBehavior
{
std::optional<entt::entity> currentTarget;
float maxRepairRange_tiles = 0.0f;
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
float score = 0.0f;
};

View File

@@ -10,5 +10,6 @@ struct SalvageScrapBehavior
{
std::optional<QVector2D> scrapTarget;
float maxCollectionRange_tiles = 0.0f;
float orbitRadius_tiles = 0.0f; // REQ-SHP-ORBIT
float score = 0.0f;
};

View File

@@ -343,12 +343,24 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
if (!weaponChildren.empty())
{
m_admin.addComponent<AttackBehavior>(entity, AttackBehavior{});
float maxWeaponRange = 0.0f;
for (entt::entity child : weaponChildren)
{
const float r = m_admin.get<WeaponComponent>(child).range_tiles;
if (r > maxWeaponRange) { maxWeaponRange = r; }
}
AttackBehavior attack;
attack.orbitRadius_tiles =
maxWeaponRange * static_cast<float>(m_config.world.orbitFactor);
m_admin.addComponent<AttackBehavior>(entity, attack);
if (!isEnemy)
{
RallyBehavior rally;
rally.rallyPoint = m_rallyPoint;
rally.rallyPoint = m_rallyPoint;
rally.orbitRadius_tiles =
static_cast<float>(m_config.world.rallyOrbitRadius_tiles);
m_admin.addComponent<RallyBehavior>(entity, rally);
}
}
@@ -365,6 +377,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
SalvageScrapBehavior salvage;
salvage.scrapTarget = std::nullopt;
salvage.maxCollectionRange_tiles = maxCollRange;
salvage.orbitRadius_tiles =
maxCollRange * static_cast<float>(m_config.world.orbitFactor);
m_admin.addComponent<SalvageScrapBehavior>(entity, salvage);
DeliverScrapBehavior deliver;
@@ -384,6 +398,8 @@ entt::entity ShipSystem::spawn(const std::string& schematicId, int level,
RepairBehavior repair;
repair.currentTarget = std::nullopt;
repair.maxRepairRange_tiles = maxRepairRange;
repair.orbitRadius_tiles =
maxRepairRange * static_cast<float>(m_config.world.orbitFactor);
m_admin.addComponent<RepairBehavior>(entity, repair);
}

View File

@@ -5,6 +5,7 @@
#include "EntityAdmin.h"
#include "ModuleOwnerComponent.h"
#include "MovementIntentComponent.h"
#include "OrbitMath.h"
#include "PositionComponent.h"
#include "SelectedBehaviorComponent.h"
#include "tracing.h"
@@ -28,7 +29,9 @@ void AttackExecutor::execute(EntityAdmin& admin)
QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
{
dest = admin.get<PositionComponent>(t).value;
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
dest = OrbitMath::computeOrbitDestination(pos.value, targetPos,
attack.orbitRadius_tiles);
}
intent = MovementIntentComponent{true, dest};
});

View File

@@ -0,0 +1,45 @@
#pragma once
#include <cmath>
#include <QVector2D>
// Orbit movement helper (REQ-SHP-ORBIT). Behaviors that keep a ship circling a
// target (attack, repair, salvage, rally) feed the result of this function as the
// movement intent destination instead of the target's center.
namespace OrbitMath
{
// Lead angle (radians) by which the radial direction is rotated to produce
// tangential motion. The fixed positive (counter-clockwise) sense makes the
// orbit direction stable for the duration of orbiting a given target.
constexpr float kOrbitLeadAngle_rad = 0.6f;
// Returns a destination on the orbit circle of `radius` around `target`. The
// result always lies exactly `radius` from `target`, so steering toward it
// both corrects the standoff distance and advances the ship tangentially.
// A radius of zero or less falls back to the target center (legacy "approach
// the target" behavior), e.g. when the ship has no tool range to orbit at.
inline QVector2D computeOrbitDestination(const QVector2D& shipPos,
const QVector2D& target, float radius)
{
if (radius <= 0.0f) { return target; }
QVector2D radial = shipPos - target;
float length = radial.length();
if (length < 1.0e-4f)
{
// Ship sits on the target; pick an arbitrary radial direction.
radial = QVector2D(1.0f, 0.0f);
length = 1.0f;
}
const QVector2D radialDirection = radial / length;
const float cosLead = std::cos(kOrbitLeadAngle_rad);
const float sinLead = std::sin(kOrbitLeadAngle_rad);
const QVector2D leadDirection(
radialDirection.x() * cosLead - radialDirection.y() * sinLead,
radialDirection.x() * sinLead + radialDirection.y() * cosLead);
return target + radius * leadDirection;
}
}

View File

@@ -3,6 +3,8 @@
#include "BehaviorKind.h"
#include "EntityAdmin.h"
#include "MovementIntentComponent.h"
#include "OrbitMath.h"
#include "PositionComponent.h"
#include "RallyBehavior.h"
#include "SelectedBehaviorComponent.h"
#include "tracing.h"
@@ -10,11 +12,15 @@
void RallyExecutor::execute(EntityAdmin& admin)
{
TRACE();
admin.forEach<RallyBehavior, SelectedBehaviorComponent, MovementIntentComponent>(
admin.forEach<RallyBehavior, SelectedBehaviorComponent, PositionComponent,
MovementIntentComponent>(
[](entt::entity /*e*/, const RallyBehavior& rally,
const SelectedBehaviorComponent& selected, MovementIntentComponent& intent)
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
MovementIntentComponent& intent)
{
if (selected.winner != BehaviorKind::Rally) { return; }
intent = MovementIntentComponent{true, rally.rallyPoint};
const QVector2D dest = OrbitMath::computeOrbitDestination(
pos.value, rally.rallyPoint, rally.orbitRadius_tiles);
intent = MovementIntentComponent{true, dest};
});
}

View File

@@ -4,6 +4,7 @@
#include "EntityAdmin.h"
#include "ModuleOwnerComponent.h"
#include "MovementIntentComponent.h"
#include "OrbitMath.h"
#include "PositionComponent.h"
#include "RepairBehavior.h"
#include "RepairToolComponent.h"
@@ -28,7 +29,9 @@ void RepairExecutor::execute(EntityAdmin& admin)
QVector2D dest = pos.value;
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
{
dest = admin.get<PositionComponent>(t).value;
const QVector2D targetPos = admin.get<PositionComponent>(t).value;
dest = OrbitMath::computeOrbitDestination(pos.value, targetPos,
repair.orbitRadius_tiles);
}
intent = MovementIntentComponent{true, dest};
});

View File

@@ -3,6 +3,8 @@
#include "BehaviorKind.h"
#include "EntityAdmin.h"
#include "MovementIntentComponent.h"
#include "OrbitMath.h"
#include "PositionComponent.h"
#include "SalvageScrapBehavior.h"
#include "SelectedBehaviorComponent.h"
#include "tracing.h"
@@ -10,12 +12,16 @@
void SalvageScrapExecutor::execute(EntityAdmin& admin)
{
TRACE();
admin.forEach<SalvageScrapBehavior, SelectedBehaviorComponent, MovementIntentComponent>(
admin.forEach<SalvageScrapBehavior, SelectedBehaviorComponent, PositionComponent,
MovementIntentComponent>(
[](entt::entity /*e*/, const SalvageScrapBehavior& salvage,
const SelectedBehaviorComponent& selected, MovementIntentComponent& intent)
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
MovementIntentComponent& intent)
{
if (selected.winner != BehaviorKind::SalvageScrap) { return; }
if (!salvage.scrapTarget) { return; }
intent = MovementIntentComponent{true, *salvage.scrapTarget};
const QVector2D dest = OrbitMath::computeOrbitDestination(
pos.value, *salvage.scrapTarget, salvage.orbitRadius_tiles);
intent = MovementIntentComponent{true, dest};
});
}