Add Metalstorm-style cursor aim

Virtual stick cursor steered through the gun reticle, server-side virtual pilot
using measured control responses, client cursor/camera handling with look-ahead,
free look and scroll zoom.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YVREx3uo7nz1FEhzveWTUh
This commit is contained in:
Claude committed 2026-11-11 13:38:11 +01:00
1 parent 6a8a7ead04
commit 44a4390f69
17 files changed
+1037 -38

No files matched your search

@@ -27,6 +27,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
private static final double INPUT_RATE_PER_TICK = 0.2;
private static final double VELOCITY_EPSILON = 0.05;
private static final double ANGULAR_EPSILON = 0.005;
private static final ControlAxis[] MEASURED_AXES = {ControlAxis.PITCH, ControlAxis.YAW, ControlAxis.ROLL};
private final SubstepClock clock;
@@ -57,9 +58,22 @@ public final class AeroState implements PhysicsBridge.Publishable {
// Torque (ship axes) one unit of pitch input on this node's pitch surfaces makes, measured every substep
final Vector3d pitchResponse = new Vector3d();
final Vector3d pitchResponseBuilding = new Vector3d();
// The same for yaw and roll, measured while cursor aim flies the node
final Vector3d yawResponse = new Vector3d();
final Vector3d yawResponseBuilding = new Vector3d();
final Vector3d rollResponse = new Vector3d();
final Vector3d rollResponseBuilding = new Vector3d();
final AutoTrim.Targets trimTargets = new AutoTrim.Targets();
volatile double savedTrim;
Vector3d responseBuilding(ControlAxis axis) {
return switch (axis) {
case YAW -> yawResponseBuilding;
case ROLL -> rollResponseBuilding;
default -> pitchResponseBuilding;
};
}
NodeState(NodeData data) {
pitch = data.pitch();
yaw = data.yaw();
@@ -211,6 +225,8 @@ public final class AeroState implements PhysicsBridge.Publishable {
wingStalled = false;
for (NodeState state : nodeStates.values()) {
state.pitchResponseBuilding.zero();
state.yawResponseBuilding.zero();
state.rollResponseBuilding.zero();
}
}
@@ -221,7 +237,7 @@ public final class AeroState implements PhysicsBridge.Publishable {
r[2] = s.z() - centerOfMass.z();
NodeData node = s.node() != AeroSurface.NO_NODE ? frame.nodes.get(s.node()) : null;
NodeState state = node != null ? nodeStates.get(node.key()) : null;
double input = s.type().controllable ? input(s, node, state, 0.0) : 0.0;
double input = s.type().controllable ? input(s, node, state, ControlAxis.NONE, 0.0) : 0.0;
if (improved) {
boolean nowStalled = ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], out);
@@ -233,53 +249,62 @@ public final class AeroState implements PhysicsBridge.Publishable {
WariumAero.evaluate(s, v, w, r, input, weight, out);
}
// How much a little more pitch input on this node would change the torque, for auto trim
if (commit && node != null && node.autoTrim() && s.type().controllable && s.listensToPitch()) {
double delta = 0.05;
probe.clear();
double base = 0.0;
if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, input, weight, probe);
// How much a little more input on an axis would change the torque: pitch for auto trim, all three for
// cursor aim
if (commit && node != null && s.type().controllable) {
boolean aimed = CursorAimRegistry.isActive(node.key());
for (ControlAxis axis : MEASURED_AXES) {
if (!s.listensTo(axis) || !(aimed || (axis == ControlAxis.PITCH && node.autoTrim()))) {
continue;
}
double delta = 0.05;
probe.clear();
if (improved) {
ImprovedAero.evaluate(s, v, w, r, input, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, input, weight, probe);
}
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
probe.clear();
double nudged = input(s, node, state, axis, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
state.responseBuilding(axis).add(
(probe.totalTorque(0) - before[0]) / delta,
(probe.totalTorque(1) - before[1]) / delta,
(probe.totalTorque(2) - before[2]) / delta);
}
double[] before = {probe.totalTorque(0), probe.totalTorque(1), probe.totalTorque(2)};
probe.clear();
double nudged = input(s, node, state, delta);
if (improved) {
ImprovedAero.evaluate(s, v, w, r, nudged, weight, tuning, stalled[i], probe);
} else {
WariumAero.evaluate(s, v, w, r, nudged, weight, probe);
}
state.pitchResponseBuilding.add(
(probe.totalTorque(0) - before[0]) / delta,
(probe.totalTorque(1) - before[1]) / delta,
(probe.totalTorque(2) - before[2]) / delta);
}
}
if (commit) {
for (NodeState state : nodeStates.values()) {
state.pitchResponse.set(state.pitchResponseBuilding);
state.yawResponse.set(state.yawResponseBuilding);
state.rollResponse.set(state.rollResponseBuilding);
}
}
}
/** A control surface's input, combined from its two keys and clamped like Warium does. */
private static double input(AeroSurface s, NodeData node, NodeState state, double extraPitch) {
private static double input(AeroSurface s, NodeData node, NodeState state, ControlAxis nudged, double extra) {
if (node == null || state == null) {
return 0.0;
}
double value = axisValue(s.axis1(), node, state, extraPitch) * s.multiplier1()
+ axisValue(s.axis2(), node, state, extraPitch) * s.multiplier2();
double value = axisValue(s.axis1(), node, state, nudged, extra) * s.multiplier1()
+ axisValue(s.axis2(), node, state, nudged, extra) * s.multiplier2();
return Math.max(-1.0, Math.min(1.0, value));
}
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, double extraPitch) {
private static double axisValue(ControlAxis axis, NodeData node, NodeState state, ControlAxis nudged, double extra) {
double nudge = axis == nudged ? extra : 0.0;
return switch (axis) {
case PITCH -> state.pitch + state.trimOutput + extraPitch;
case YAW -> state.yaw;
case ROLL -> state.roll;
case PITCH -> state.pitch + state.trimOutput + nudge;
case YAW -> state.yaw + nudge;
case ROLL -> state.roll + nudge;
case THROTTLE -> node.throttle();
case NONE -> 0.0;
};
@@ -293,10 +318,12 @@ public final class AeroState implements PhysicsBridge.Publishable {
if (state == null) {
continue;
}
state.pitch = moveTowards(state.pitch, node.pitch(), step);
state.yaw = moveTowards(state.yaw, node.yaw(), step);
state.roll = moveTowards(state.roll, node.roll(), step);
AutoTrim.update(state, node, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
// Cursor aim, if the pilot has it on, supplies the inputs the pilot is not giving with the keys
NodeData effective = CursorPilot.apply(state, node, rotation, inertia, w, worldVelocity.length(), wingStalled);
state.pitch = moveTowards(state.pitch, effective.pitch(), step);
state.yaw = moveTowards(state.yaw, effective.yaw(), step);
state.roll = moveTowards(state.roll, effective.roll(), step);
AutoTrim.update(state, effective, worldVelocity, altitude, rotation, inertia, w, dt, improved && wingStalled);
}
}