change to physics based ship movement
This commit is contained in:
@@ -16,6 +16,10 @@ hp_formula = "3"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "4"
|
speed_formula = "4"
|
||||||
|
main_acceleration_formula = "8"
|
||||||
|
maneuvering_acceleration_formula = "4"
|
||||||
|
angular_acceleration_formula = "12.56"
|
||||||
|
max_rotation_speed_formula = "6.28"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "15"
|
sensor_range_formula = "15"
|
||||||
@@ -48,6 +52,10 @@ hp_formula = "8"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "1"
|
speed_formula = "1"
|
||||||
|
main_acceleration_formula = "1.5"
|
||||||
|
maneuvering_acceleration_formula = "0.5"
|
||||||
|
angular_acceleration_formula = "9.42"
|
||||||
|
max_rotation_speed_formula = "3.14"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "25"
|
sensor_range_formula = "25"
|
||||||
@@ -80,6 +88,10 @@ hp_formula = "12"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "1"
|
speed_formula = "1"
|
||||||
|
main_acceleration_formula = "1.5"
|
||||||
|
maneuvering_acceleration_formula = "0.5"
|
||||||
|
angular_acceleration_formula = "15.7"
|
||||||
|
max_rotation_speed_formula = "3.14"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "20"
|
sensor_range_formula = "20"
|
||||||
@@ -112,6 +124,10 @@ hp_formula = "40 + 4*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "110"
|
speed_formula = "110"
|
||||||
|
main_acceleration_formula = "220"
|
||||||
|
maneuvering_acceleration_formula = "110"
|
||||||
|
angular_acceleration_formula = "12.56"
|
||||||
|
max_rotation_speed_formula = "6.28"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
@@ -142,6 +158,10 @@ hp_formula = "60 + 5*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "130"
|
speed_formula = "130"
|
||||||
|
main_acceleration_formula = "260"
|
||||||
|
maneuvering_acceleration_formula = "130"
|
||||||
|
angular_acceleration_formula = "12.56"
|
||||||
|
max_rotation_speed_formula = "6.28"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ hp_formula = "40 + 5*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "200 + 5*x"
|
speed_formula = "200 + 5*x"
|
||||||
|
main_acceleration_formula = "100000"
|
||||||
|
maneuvering_acceleration_formula = "100000"
|
||||||
|
angular_acceleration_formula = "100000"
|
||||||
|
max_rotation_speed_formula = "100000"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "200"
|
sensor_range_formula = "200"
|
||||||
@@ -47,6 +51,10 @@ hp_formula = "120 + 15*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "120"
|
speed_formula = "120"
|
||||||
|
main_acceleration_formula = "100000"
|
||||||
|
maneuvering_acceleration_formula = "100000"
|
||||||
|
angular_acceleration_formula = "100000"
|
||||||
|
max_rotation_speed_formula = "100000"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "300"
|
sensor_range_formula = "300"
|
||||||
@@ -78,6 +86,10 @@ hp_formula = "40 + 4*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "110"
|
speed_formula = "110"
|
||||||
|
main_acceleration_formula = "100000"
|
||||||
|
maneuvering_acceleration_formula = "100000"
|
||||||
|
angular_acceleration_formula = "100000"
|
||||||
|
max_rotation_speed_formula = "100000"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
@@ -108,6 +120,10 @@ hp_formula = "60 + 5*x"
|
|||||||
|
|
||||||
[ship.movement]
|
[ship.movement]
|
||||||
speed_formula = "130"
|
speed_formula = "130"
|
||||||
|
main_acceleration_formula = "100000"
|
||||||
|
maneuvering_acceleration_formula = "100000"
|
||||||
|
angular_acceleration_formula = "100000"
|
||||||
|
max_rotation_speed_formula = "100000"
|
||||||
|
|
||||||
[ship.sensor]
|
[ship.sensor]
|
||||||
sensor_range_formula = "250"
|
sensor_range_formula = "250"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Config files use the TOML format. The following config files drive game paramete
|
|||||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
||||||
- **buildings.toml** — building block cost and construction time per building type.
|
- **buildings.toml** — building block cost and construction time per building type.
|
||||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
|
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
|
||||||
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), ship stats (HP, speed, damage, attack range, attack rate, sensor range) as formulas of ship level, required build materials, threat cost formula, player production level, whether the schematic is available from game start, and a layout grid defining the ship's module slots.
|
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), ship stats (HP, max linear speed, damage, attack range, attack rate, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, whether the schematic is available from game start, and a layout grid defining the ship's module slots.
|
||||||
- **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and stat modifier formulas (additive and/or multiplicative per stat).
|
- **modules.toml** — per module type: id, surface mask, materials list, player production level, production time, threat cost, fill color, glyph, and stat modifier formulas (additive and/or multiplicative per stat).
|
||||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship role, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
@@ -149,10 +149,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
## Ships
|
## Ships
|
||||||
|
|
||||||
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
- REQ-SHP-AUTONOMOUS: Ships are produced by shipyards and are fully autonomous once produced.
|
||||||
- REQ-SHP-STATS: Base ship stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), speed (`[ship.movement].speed_formula`), damage (`[ship.combat].damage_formula`), attack range (`[ship.combat].attack_range_formula`), attack rate (`[ship.combat].attack_rate_formula`), sensor range (`[ship.sensors].range_formula`). Required build materials (`[ship.schematic].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. Final ship stats incorporate module modifiers per REQ-MOD-STAT-CALC.
|
- REQ-SHP-STATS: Base ship stats are defined as formulas of ship level in `ships.toml`: HP (`[ship.health].hp_formula`), max linear speed (`[ship.movement].speed_formula`), damage (`[ship.combat].damage_formula`), attack range (`[ship.combat].attack_range_formula`), attack rate (`[ship.combat].attack_rate_formula`), sensor range (`[ship.sensors].range_formula`), main acceleration (`[ship.movement].main_acceleration_formula`, tiles/s²), maneuvering acceleration (`[ship.movement].maneuvering_acceleration_formula`, tiles/s²), angular acceleration (`[ship.movement].angular_acceleration_formula`, rad/s²), max rotation speed (`[ship.movement].max_rotation_speed_formula`, rad/s). Required build materials (`[ship.schematic].materials`) and availability from game start (`[[ship]].available_from_start`) are also defined there. Final ship stats incorporate module modifiers per REQ-MOD-STAT-CALC.
|
||||||
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
- REQ-SHP-SPAWN-PLAYER: A ship produced by a shipyard spawns centered on the shipyard's output port tile.
|
||||||
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
- REQ-SHP-SPAWN-ENEMY: Enemy ships spawn at a uniformly random position within the current enemy buffer zone — random X across the buffer's width and random Y across the world height.
|
||||||
- REQ-SHP-MOVEMENT: Ships move in straight lines toward their current destination at the speed defined by their speed formula. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
- REQ-SHP-MOVEMENT: Ships move using a physics-based model. Each ship has a velocity and a facing direction, both updated each tick. The main acceleration (`main_acceleration_formula`) is applied along the ship's current facing direction only. The maneuvering acceleration (`maneuvering_acceleration_formula`) can be applied in any direction independently of the facing direction, enabling lateral or braking movement without rotating. The angular acceleration (`angular_acceleration_formula`) controls how quickly the ship rotates. Linear speed is capped at the ship's `speed_formula` value; rotation rate is capped at the ship's `max_rotation_speed_formula` value. Ship position refers to the ship's center for all range, sensor, and attack checks.
|
||||||
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
- REQ-SHP-NO-COLLISION: Ships do not collide with each other or with defence stations; they may visually overlap.
|
||||||
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
- REQ-SHP-SENSOR: A ship perceives only entities within its sensor range. Behavior is driven by what is in sensor range; entities outside sensor range are ignored.
|
||||||
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
- REQ-SHP-FIRING: All weapons — on ships and on defence stations — fire when off cooldown and the target is within attack range. Firing emits a fire event and starts a 0.15-second damage delay (half the beam duration). When that delay expires, damage is applied to the target — unless the target has already been destroyed, in which case the damage is silently dropped. If the shooter is destroyed before the delay expires, damage is still applied when the delay expires. There is no projectile entity and no intervening collision. The weapon's cooldown begins at the moment of firing, not at damage application.
|
||||||
|
|||||||
@@ -396,6 +396,10 @@ ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|||||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
||||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||||
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
def.movement.speedFormula = requireFormula(mMt["speed_formula"], file, mPath + ".speed_formula");
|
||||||
|
def.movement.mainAccelerationFormula = requireFormula(mMt["main_acceleration_formula"], file, mPath + ".main_acceleration_formula");
|
||||||
|
def.movement.maneuveringAccelerationFormula = requireFormula(mMt["maneuvering_acceleration_formula"], file, mPath + ".maneuvering_acceleration_formula");
|
||||||
|
def.movement.angularAccelerationFormula = requireFormula(mMt["angular_acceleration_formula"], file, mPath + ".angular_acceleration_formula");
|
||||||
|
def.movement.maxRotationSpeedFormula = requireFormula(mMt["max_rotation_speed_formula"], file, mPath + ".max_rotation_speed_formula");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sensor
|
// Sensor
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ struct ShipHealth
|
|||||||
|
|
||||||
struct ShipMovement
|
struct ShipMovement
|
||||||
{
|
{
|
||||||
Formula speedFormula; // REQ-SHP-STATS, REQ-SHP-MOVEMENT
|
Formula speedFormula; // max linear speed cap, tiles/s (REQ-SHP-STATS, REQ-SHP-MOVEMENT)
|
||||||
|
Formula mainAccelerationFormula; // forward acceleration, tiles/s²
|
||||||
|
Formula maneuveringAccelerationFormula;// omnidirectional acceleration, tiles/s²
|
||||||
|
Formula angularAccelerationFormula; // angular acceleration, rad/s²
|
||||||
|
Formula maxRotationSpeedFormula; // angular velocity cap, rad/s
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ShipSensor
|
struct ShipSensor
|
||||||
|
|||||||
@@ -75,9 +75,15 @@ struct Ship
|
|||||||
EntityId id;
|
EntityId id;
|
||||||
QVector2D position;
|
QVector2D position;
|
||||||
QVector2D velocity;
|
QVector2D velocity;
|
||||||
|
float facing; // heading in radians (0 = east/+x)
|
||||||
|
float rotationSpeed; // angular velocity in radians per tick
|
||||||
float hp;
|
float hp;
|
||||||
float maxHp;
|
float maxHp;
|
||||||
float speedPerTick; // pre-evaluated from speedFormula / kTickRateHz
|
float maxSpeedPerTick; // linear speed cap (tiles/tick)
|
||||||
|
float mainAccelerationPerTick; // forward acceleration (tiles/tick²)
|
||||||
|
float maneuveringAccelerationPerTick; // omnidirectional acceleration (tiles/tick²)
|
||||||
|
float angularAccelerationPerTick; // angular acceleration (rad/tick²)
|
||||||
|
float maxRotationSpeedPerTick; // angular velocity cap (rad/tick)
|
||||||
float sensorRange; // pre-evaluated from sensorRangeFormula (REQ-SHP-SENSOR)
|
float sensorRange; // pre-evaluated from sensorRangeFormula (REQ-SHP-SENSOR)
|
||||||
int level;
|
int level;
|
||||||
std::string schematicId;
|
std::string schematicId;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -58,11 +59,21 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
|||||||
ship.id = m_allocateId();
|
ship.id = m_allocateId();
|
||||||
ship.position = position;
|
ship.position = position;
|
||||||
ship.velocity = QVector2D(0.0f, 0.0f);
|
ship.velocity = QVector2D(0.0f, 0.0f);
|
||||||
|
ship.facing = 0.0f;
|
||||||
|
ship.rotationSpeed = 0.0f;
|
||||||
ship.maxHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
ship.maxHp = static_cast<float>(def->health.hpFormula.evaluate(x));
|
||||||
ship.hp = ship.maxHp;
|
ship.hp = ship.maxHp;
|
||||||
ship.speedPerTick = static_cast<float>(
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
def->movement.speedFormula.evaluate(x))
|
ship.maxSpeedPerTick = static_cast<float>(def->movement.speedFormula.evaluate(x))
|
||||||
/ static_cast<float>(kTickRateHz);
|
/ tickRate;
|
||||||
|
ship.mainAccelerationPerTick = static_cast<float>(def->movement.mainAccelerationFormula.evaluate(x))
|
||||||
|
/ tickRate;
|
||||||
|
ship.maneuveringAccelerationPerTick = static_cast<float>(def->movement.maneuveringAccelerationFormula.evaluate(x))
|
||||||
|
/ tickRate;
|
||||||
|
ship.angularAccelerationPerTick = static_cast<float>(def->movement.angularAccelerationFormula.evaluate(x))
|
||||||
|
/ tickRate;
|
||||||
|
ship.maxRotationSpeedPerTick = static_cast<float>(def->movement.maxRotationSpeedFormula.evaluate(x))
|
||||||
|
/ tickRate;
|
||||||
ship.sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
|
ship.sensorRange = static_cast<float>(def->sensor.sensorRangeFormula.evaluate(x));
|
||||||
ship.level = level;
|
ship.level = level;
|
||||||
ship.schematicId = schematicId;
|
ship.schematicId = schematicId;
|
||||||
@@ -150,7 +161,11 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
|
|||||||
|
|
||||||
applyMod(ship.maxHp, "hp");
|
applyMod(ship.maxHp, "hp");
|
||||||
ship.hp = ship.maxHp;
|
ship.hp = ship.maxHp;
|
||||||
applyMod(ship.speedPerTick, "speed");
|
applyMod(ship.maxSpeedPerTick, "speed");
|
||||||
|
applyMod(ship.mainAccelerationPerTick, "main_acceleration");
|
||||||
|
applyMod(ship.maneuveringAccelerationPerTick, "maneuvering_acceleration");
|
||||||
|
applyMod(ship.angularAccelerationPerTick, "angular_acceleration");
|
||||||
|
applyMod(ship.maxRotationSpeedPerTick, "max_rotation_speed");
|
||||||
applyMod(ship.sensorRange, "sensor_range");
|
applyMod(ship.sensorRange, "sensor_range");
|
||||||
if (ship.weapon.has_value())
|
if (ship.weapon.has_value())
|
||||||
{
|
{
|
||||||
@@ -764,6 +779,16 @@ void ShipSystem::triggerRallyDeparture()
|
|||||||
// tickMovement (tick-order step 10)
|
// tickMovement (tick-order step 10)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Reduces angle to [-π, π].
|
||||||
|
static float wrapAngle(float a)
|
||||||
|
{
|
||||||
|
constexpr float kPi = 3.14159265f;
|
||||||
|
a = std::fmod(a, 2.0f * kPi);
|
||||||
|
if (a > kPi) { a -= 2.0f * kPi; }
|
||||||
|
if (a < -kPi) { a += 2.0f * kPi; }
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
void ShipSystem::tickMovement()
|
void ShipSystem::tickMovement()
|
||||||
{
|
{
|
||||||
for (Ship& s : m_ships)
|
for (Ship& s : m_ships)
|
||||||
@@ -771,18 +796,81 @@ void ShipSystem::tickMovement()
|
|||||||
if (s.intent.priority == 0)
|
if (s.intent.priority == 0)
|
||||||
{
|
{
|
||||||
s.velocity = QVector2D(0.0f, 0.0f);
|
s.velocity = QVector2D(0.0f, 0.0f);
|
||||||
|
s.rotationSpeed = 0.0f;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
QVector2D delta = s.intent.target - s.position;
|
|
||||||
float dist = delta.length();
|
const QVector2D delta = s.intent.target - s.position;
|
||||||
if (dist <= s.speedPerTick)
|
const float dist = delta.length();
|
||||||
|
|
||||||
|
if (dist < 0.001f)
|
||||||
|
{
|
||||||
|
s.velocity = QVector2D(0.0f, 0.0f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rotate toward target ──────────────────────────────────────────
|
||||||
|
const float desiredAngle = std::atan2(delta.y(), delta.x());
|
||||||
|
const float angleDiff = wrapAngle(desiredAngle - s.facing);
|
||||||
|
|
||||||
|
// Clamp angular acceleration, accumulate rotation speed.
|
||||||
|
const float rotDelta = std::max(-s.angularAccelerationPerTick,
|
||||||
|
std::min(angleDiff, s.angularAccelerationPerTick));
|
||||||
|
s.rotationSpeed += rotDelta;
|
||||||
|
s.rotationSpeed = std::max(-s.maxRotationSpeedPerTick,
|
||||||
|
std::min(s.rotationSpeed, s.maxRotationSpeedPerTick));
|
||||||
|
|
||||||
|
// Prevent overshooting the desired angle this tick.
|
||||||
|
const bool sameSign = (s.rotationSpeed >= 0.0f) == (angleDiff >= 0.0f);
|
||||||
|
if (sameSign && std::abs(s.rotationSpeed) > std::abs(angleDiff))
|
||||||
|
{
|
||||||
|
s.rotationSpeed = angleDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
s.facing = wrapAngle(s.facing + s.rotationSpeed);
|
||||||
|
|
||||||
|
// ── Desired velocity (with braking near target) ───────────────────
|
||||||
|
// Stopping distance using maneuvering acceleration as the worst-case brake.
|
||||||
|
const float manAccel = s.maneuveringAccelerationPerTick;
|
||||||
|
const float stoppingDist = (s.maxSpeedPerTick * s.maxSpeedPerTick)
|
||||||
|
/ (2.0f * manAccel);
|
||||||
|
const float desiredSpeed = (dist <= stoppingDist)
|
||||||
|
? std::sqrt(2.0f * manAccel * dist)
|
||||||
|
: s.maxSpeedPerTick;
|
||||||
|
const QVector2D desiredVel = delta.normalized() * desiredSpeed;
|
||||||
|
const QVector2D velError = desiredVel - s.velocity;
|
||||||
|
|
||||||
|
// ── Main acceleration: forward only, along facing ─────────────────
|
||||||
|
const QVector2D facingVec(std::cos(s.facing), std::sin(s.facing));
|
||||||
|
const float mainAligned = std::max(0.0f,
|
||||||
|
QVector2D::dotProduct(velError, facingVec));
|
||||||
|
const float mainApplied = std::min(mainAligned, s.mainAccelerationPerTick);
|
||||||
|
const QVector2D mainDelta = facingVec * mainApplied;
|
||||||
|
|
||||||
|
// ── Maneuvering acceleration: any direction, handles the remainder ─
|
||||||
|
const QVector2D remaining = velError - mainDelta;
|
||||||
|
const float remainLen = remaining.length();
|
||||||
|
const QVector2D maneuverDelta = (remainLen > manAccel)
|
||||||
|
? remaining.normalized() * manAccel
|
||||||
|
: remaining;
|
||||||
|
|
||||||
|
s.velocity += mainDelta + maneuverDelta;
|
||||||
|
|
||||||
|
// ── Speed cap ─────────────────────────────────────────────────────
|
||||||
|
const float speed = s.velocity.length();
|
||||||
|
if (speed > s.maxSpeedPerTick)
|
||||||
|
{
|
||||||
|
s.velocity = s.velocity.normalized() * s.maxSpeedPerTick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Snap to target or advance ─────────────────────────────────────
|
||||||
|
if (dist <= s.velocity.length())
|
||||||
{
|
{
|
||||||
s.position = s.intent.target;
|
s.position = s.intent.target;
|
||||||
s.velocity = QVector2D(0.0f, 0.0f);
|
s.velocity = QVector2D(0.0f, 0.0f);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
s.velocity = delta.normalized() * s.speedPerTick;
|
|
||||||
s.position += s.velocity;
|
s.position += s.velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,13 +93,15 @@ TEST_CASE("BehaviorSystem: clearMovementIntents resets all ships to priority 0",
|
|||||||
// tickMovement
|
// tickMovement
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
TEST_CASE("BehaviorSystem: tickMovement advances ship by speedPerTick toward target",
|
// With facing=0 and target due east, main thrust drives the ship east. The test
|
||||||
|
// config uses very high thrust so the ship reaches maxSpeedPerTick in one tick.
|
||||||
|
TEST_CASE("BehaviorSystem: tickMovement advances ship by maxSpeedPerTick toward target",
|
||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
const EntityId id = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const EntityId id = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
const float speed = f.ships.findShip(id)->speedPerTick;
|
const float speed = f.ships.findShip(id)->maxSpeedPerTick;
|
||||||
const QVector2D target(100.0f, 0.0f);
|
const QVector2D target(100.0f, 0.0f);
|
||||||
|
|
||||||
f.ships.forEach([&target](Ship& s) {
|
f.ships.forEach([&target](Ship& s) {
|
||||||
@@ -112,6 +114,8 @@ TEST_CASE("BehaviorSystem: tickMovement advances ship by speedPerTick toward tar
|
|||||||
REQUIRE(s->position.y() == Approx(0.0f));
|
REQUIRE(s->position.y() == Approx(0.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With very high maneuvering thrust the stopping distance is ~0, so desiredSpeed
|
||||||
|
// still exceeds maxSpeedPerTick and the snap-to-target branch fires.
|
||||||
TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoot",
|
TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoot",
|
||||||
"[behavior]")
|
"[behavior]")
|
||||||
{
|
{
|
||||||
@@ -119,7 +123,7 @@ TEST_CASE("BehaviorSystem: tickMovement stops exactly at target without overshoo
|
|||||||
const EntityId id = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
const EntityId id = f.ships.spawn("interceptor", 1, QVector2D(0.0f, 0.0f));
|
||||||
|
|
||||||
// Place target closer than one tick's travel.
|
// Place target closer than one tick's travel.
|
||||||
const float speed = f.ships.findShip(id)->speedPerTick;
|
const float speed = f.ships.findShip(id)->maxSpeedPerTick;
|
||||||
const QVector2D target(speed * 0.5f, 0.0f);
|
const QVector2D target(speed * 0.5f, 0.0f);
|
||||||
|
|
||||||
f.ships.forEach([&target](Ship& s) {
|
f.ships.forEach([&target](Ship& s) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ TEST_CASE("ShipSystem: interceptor level 5 hp matches formula", "[ship]")
|
|||||||
REQUIRE(ship->maxHp == Approx(65.0f));
|
REQUIRE(ship->maxHp == Approx(65.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("ShipSystem: interceptor level 0 speedPerTick matches formula / kTickRateHz", "[ship]")
|
TEST_CASE("ShipSystem: interceptor level 0 maxSpeedPerTick matches formula / kTickRateHz", "[ship]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadConfig();
|
||||||
EntityId nextId = 1;
|
EntityId nextId = 1;
|
||||||
@@ -86,9 +86,9 @@ TEST_CASE("ShipSystem: interceptor level 0 speedPerTick matches formula / kTickR
|
|||||||
const EntityId id = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
|
const EntityId id = ss.spawn("interceptor", 0, QVector2D(0.0f, 0.0f));
|
||||||
const Ship* ship = ss.findShip(id);
|
const Ship* ship = ss.findShip(id);
|
||||||
|
|
||||||
// speed_formula = "200 + 5*x" at x=0 → 200; speedPerTick = 200/30
|
// speed_formula = "200 + 5*x" at x=0 → 200; maxSpeedPerTick = 200/30
|
||||||
const float expected = 200.0f / static_cast<float>(kTickRateHz);
|
const float expected = 200.0f / static_cast<float>(kTickRateHz);
|
||||||
REQUIRE(ship->speedPerTick == Approx(expected));
|
REQUIRE(ship->maxSpeedPerTick == Approx(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -810,10 +810,7 @@ void GameWorldView::drawShips(QPainter& painter)
|
|||||||
if (it == m_visuals->ships.end()) { continue; }
|
if (it == m_visuals->ships.end()) { continue; }
|
||||||
|
|
||||||
const QPointF center = worldToWidget(ship.position);
|
const QPointF center = worldToWidget(ship.position);
|
||||||
const QVector2D vel = ship.velocity;
|
const QVector2D dir(std::cos(ship.facing), std::sin(ship.facing));
|
||||||
const QVector2D dir = (vel.length() > 0.0001f)
|
|
||||||
? vel.normalized()
|
|
||||||
: QVector2D(1.0f, 0.0f);
|
|
||||||
const QVector2D perp(-dir.y(), dir.x());
|
const QVector2D perp(-dir.y(), dir.x());
|
||||||
|
|
||||||
const float fwd = tilePx() * 0.45f;
|
const float fwd = tilePx() * 0.45f;
|
||||||
|
|||||||
Reference in New Issue
Block a user