Aim at infinity along the view's pitch; delete the terrain intersection

The horizon fault was the level XZ projection, not the distance (operator, 2026-10-29). The untargeted look point is 10000 along the aim again, so the camera no longer jumps where the view ray crosses a terrain discontinuity; group aim points and crosshairs lift each planar firing bearing by the aim's pitch. Reverts the surface march of aba02182b and the field-height crosshair of a9bbea509.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-29 08:45:37 +01:00
commit 7a3c0c8fb8
2 changed files with 13 additions and 44 deletions

View file

@ -1470,17 +1470,19 @@ public class ActionGameManager : MonoBehaviour
else if (target.Entity != null)
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);
var ownField = DrawAhead.Height(CurrentEntity.Zone, ownDrawn.xz, lead);
// The aim's pitch, applied to each barrel's planar bearing: the crosshair sits where the guns point at the view's
// elevation, not on the ship's level plane (which is the horizon).
var aim = CurrentEntity.Aim;
var aimLevel = length(aim.xz);
foreach (var (_, barrels, crosshair) in _articulationGroups)
{
var averagePosition = Vector3.zero;
foreach (var barrel in barrels)
averagePosition += barrel.position + barrel.forward * distance;
{
var bearing = new Vector2(barrel.forward.x, barrel.forward.z).normalized;
averagePosition += barrel.position + new Vector3(bearing.x * aimLevel, aim.y, bearing.y * aimLevel) * distance;
}
averagePosition /= barrels.Length;
// Shots fly over the field, so the crosshair rides it too: the aim point's own field height, plus the barrels' height
// above the field under the ship. A level projection floats over downhill aim and sinks into uphill aim.
var aimPlanar = float2(averagePosition.x, averagePosition.z);
averagePosition.y += DrawAhead.Height(CurrentEntity.Zone, aimPlanar, lead) - ownField;
crosshair.Target = averagePosition;
}

View file

@ -405,10 +405,12 @@ public class EntityInstance : MonoBehaviour
// The drawn pose first: the camera's LookAt and the barrels read this frame's pose, not the last frame's.
var drawn = DrawAhead.Position(Entity, ActionGameManager.Clock.Lead);
LocalSpace.localPosition = transform.position = drawn.ToUnity();
var lead = ActionGameManager.Clock.Lead;
var aimDistance = !Entity.Target.Value.IsNone
? max(Entity.TargetRange, Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance)
: SurfaceDistance(drawn, Entity.Aim, lead);
: 10000;
// The aim's pitch: a planar firing bearing is drawn at the same elevation as the view, so a group aims where the
// player looks, not at the horizon of the ship's plane.
var aimLevel = length(Entity.Aim.xz);
LookAtPoint.position = transform.position + Entity.Aim.ToUnity() * aimDistance;
foreach (var group in _groupAims)
{
@ -416,47 +418,12 @@ public class EntityInstance : MonoBehaviour
if (weapon == null) { group.Value.position = LookAtPoint.position; continue; }
var barrel = Barrels.TryGetValue(weapon.Item.Hardpoint, out var barrels) ? barrels[0].position : transform.position;
var direction = FireControl.Solution(weapon, Entity, Entity.Target.Value).Direction;
var aimAt = barrel + new Vector3(direction.x, 0, direction.y) * aimDistance;
// Shots fly over the field: the group's aim point rides it, at the barrel's height above the field under the ship.
aimAt.y += Entity.RideHeight(float2(aimAt.x, aimAt.z), DrawAhead.Time(Entity.Zone, lead)) - drawn.y;
group.Value.position = aimAt;
group.Value.position = barrel + new Vector3(direction.x * aimLevel, Entity.Aim.y, direction.y * aimLevel) * aimDistance;
}
if (_influenceInstance)
_influenceInstance.position = new Vector3(drawn.x, 0, drawn.z);
}
// How far along `aim` from `from` the view ray meets the surface the ship rides, so an untargeted aim lands where the
// player looks on the field. Marches out in growing steps, then bisects the crossing; a ray that never comes down
// (looking level or up) aims at the horizon.
private float SurfaceDistance(CultMath.float3 from, CultMath.float3 aim, float lead)
{
const float horizon = 10000;
var time = DrawAhead.Time(Entity.Zone, lead);
float Above(float t)
{
var p = from + aim * t;
return p.y - Entity.RideHeight(p.xz, time);
}
var near = Entity.ItemManager.GameplaySettings.ConvergenceMinimumDistance;
if (Above(near) <= 0) return near;
var step = max(near, 8f);
for (var t = near; t < horizon; step *= 1.25f)
{
var next = min(t + step, horizon);
if (Above(next) <= 0)
{
for (var i = 0; i < 12; i++)
{
var mid = (t + next) / 2;
if (Above(mid) <= 0) next = mid; else t = mid;
}
return next;
}
t = next;
}
return horizon;
}
public virtual void OnDestroy()
{
if(_sensor!=null)