Combat
Targeting system
The commander's shoulder launchers never fire blind. A target is set first – a locked vehicle, a locked ground point or a map assignment – and the server checks every step.
Flow
The aim ray
The ray starts at the camera and skips the own walker: torso, arms and launchers are in the way of almost every view, so every own hit moves the start just past it and traces again.
bool AimRay(IEntity walker, IEntity localChar, out vector hit, out IEntity hitVehicle, out bool hasHit)
{
vector cam[4];
GetGame().GetWorld().GetCurrentCamera(cam);
vector dir = cam[2];
vector from = cam[3];
vector to = from + dir * m_fRange; // 2 km, more with a laser module
array<IEntity> skip = {walker, localChar};
for (int i = 0; i < 5; i++)
{
autoptr TraceParam tp = new TraceParam();
tp.Start = from;
tp.End = to;
tp.Flags = TraceFlags.ENTS | TraceFlags.WORLD | TraceFlags.OCEAN;
tp.LayerMask = EPhysicsLayerDefs.Projectile;
tp.ExcludeArray = skip;
float f = GetGame().GetWorld().TraceMove(tp, null);
if (f >= 1.0)
break;
hit = from + (to - from) * f;
IEntity e = tp.TraceEnt;
if (e && e.GetRootParent() == walker)
{
from = hit + dir * 0.3; // own part: continue behind it
continue;
}
hasHit = true;
if (e)
hitVehicle = Vehicle.Cast(e.GetRootParent());
return true;
}
hit = to;
hasHit = false;
return true;
}
Server-validated marks
The client sends ray start, direction, hit point and the hit vehicle's RplId. The server does not trust
the hit point: it must lie close to the ray (5 m + 1 % of the distance), and the ray must start near the walker.
bool PointOnRay(vector origin, vector dir, vector point)
{
vector toPoint = point - origin;
float along = vector.Dot(toPoint, dir);
if (along < 0)
return false;
float off = (toPoint - dir * along).Length();
return off <= 5.0 + 0.01 * along;
}
Then the best candidate wins, in this order: the vehicle the ray hit → the best sensor track in a narrow cone → the "next target" cursor → a ground point (needs terrain line of sight, no friendlies close by). Marking the same thing again removes it.
Locks
A marked track is not fireable immediately. Lock time grows with distance (1 s at 500 m to 8 s at 5 km), needs line of sight (unless a datalink module is fitted) and decays after 1.5 s out of sight.
float LockTime(float dist)
{
float t = Math.InverseLerp(500, 5000, dist);
return Math.Lerp(1.0, 8.0, Math.Clamp(t, 0, 1));
}
Salvo plan
A salvo walks round-robin over all fireable marks, so four rockets at four targets hit each one once. Marks can be assigned to specific weapons from the map (a bitmask per mark); a salvo of weapon X skips marks assigned elsewhere.
int Rotate(array<ref MySalvoTarget> plan)
{
int start = m_iPlanOffset % plan.Count();
for (int i = 0; i < start; i++)
{
plan.Insert(plan[0]);
plan.RemoveOrdered(0);
}
m_iPlanOffset += m_iRocketsThisSalvo;
return start;
}
The commander lock cross
Since players fired without a target, the commander now sees a small cross in the screen centre. Below it the HUD
says what the mark key would lock there – LOCK VEHICLE, LOCK POINT TRACK or
NO TARGET – followed by the key the player has bound. The binding is read at runtime, so rebinding is
reflected immediately.
string BoundKey(string action)
{
InputManager im = GetGame().GetInputManager();
EInputDeviceType dev = EInputDeviceType.KEYBOARD;
if (im.GetLastUsedInputDevice() == EInputDeviceType.GAMEPAD)
dev = EInputDeviceType.GAMEPAD;
InputBinding binding = im.CreateUserBinding();
array<string> list = {};
if (!binding.GetBindings(action, list, dev) || list.IsEmpty())
return string.Empty;
return PrettyKey(list[0]); // "keyboard:KC_I" -> "I", "gamepad0:shoulder_left+gamepad0:a" -> "LB + A"
}