cs2-css-flashlight/Flashlight/FlashlightLogic.cs
Tobias Thiele b43f9fdea3 🔦 Fix flashlight beam not tracking view pitch
The light was positioned once at toggle time and then handed to the engine
via SetParent + SetParentAttachmentMaintainOffset on the pawn's
`axis_of_intent` attachment. That attachment carries the body's yaw but not
the view pitch, so once parented the beam could only ever rotate
horizontally — looking up or down did nothing.

Drop the parenting and drive the light's transform ourselves every tick from
the pawn's live AbsOrigin and V_angle, which is how the plugin behaved before
0.1.1 replaced the per-tick teleport with a parented entity.

Also:
- OnTick no longer short-circuits on !AllowUseKey, which otherwise stopped
  the light updating for servers that only expose css_fl_toggle.
- ForwardDistance now offsets the light horizontally only. Applying it along
  the pitched forward vector put the light below ground when looking straight
  down (crouched eye height 46 minus 54).
- Sweep up the light when the pawn is dead; an un-parented entity no longer
  dies with its pawn.
- Remove the now-unused AttachmentName config key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:50:29 +02:00

115 lines
3.5 KiB
C#

using System.Numerics;
namespace Flashlight;
/// <summary>
/// Position and orientation to apply to the flashlight entity.
/// <paramref name="Angles"/> is a Source QAngle laid out as (pitch, yaw, roll).
/// </summary>
public readonly record struct LightTransform(Vector3 Origin, Vector3 Angles);
public static class FlashlightLogic
{
public static bool TryToggle(ref bool isOn, ref bool canToggle)
{
if (!canToggle)
{
return false;
}
isOn = !isOn;
canToggle = false;
return true;
}
public static bool IsUsePressedEdge(bool usePressed, bool wasUsePressed)
{
return usePressed && !wasUsePressed;
}
public static float GetEyeOffsetZ(bool isCrouching, float standOffset, float crouchOffset)
{
return isCrouching ? crouchOffset : standOffset;
}
public static Vector3 CalculateLightOrigin(
Vector3 pawnOrigin,
Vector3 forward,
float eyeOffsetZ,
float forwardDistance)
{
return new Vector3(
pawnOrigin.X + forward.X * forwardDistance,
pawnOrigin.Y + forward.Y * forwardDistance,
pawnOrigin.Z + eyeOffsetZ + forward.Z * forwardDistance);
}
public static Vector3 ForwardFromAnglesDegrees(float pitch, float yaw)
{
var pitchRad = pitch * (MathF.PI / 180f);
var yawRad = yaw * (MathF.PI / 180f);
var cosPitch = MathF.Cos(pitchRad);
return new Vector3(
cosPitch * MathF.Cos(yawRad),
cosPitch * MathF.Sin(yawRad),
-MathF.Sin(pitchRad));
}
/// <summary>
/// Forward vector on the horizontal plane only, ignoring pitch.
/// </summary>
public static Vector3 HorizontalForwardFromYawDegrees(float yaw)
{
var yawRad = yaw * (MathF.PI / 180f);
return new Vector3(MathF.Cos(yawRad), MathF.Sin(yawRad), 0f);
}
/// <summary>
/// World transform for the flashlight given the player's current pawn origin and view angles.
/// </summary>
/// <remarks>
/// The angles carry the full view rotation so the beam tracks pitch as well as yaw, while the
/// origin is only pushed forward on the horizontal plane. Offsetting the origin along the full
/// pitched forward vector would drop the light through the floor when looking straight down
/// (a crouched eye height of 46 minus a 54 unit offset ends up below the ground).
/// </remarks>
public static LightTransform CalculateLightTransform(
Vector3 pawnOrigin,
float pitch,
float yaw,
float roll,
float eyeOffsetZ,
float forwardDistance)
{
var forward = HorizontalForwardFromYawDegrees(yaw);
var origin = CalculateLightOrigin(pawnOrigin, forward, eyeOffsetZ, forwardDistance);
return new LightTransform(origin, new Vector3(pitch, yaw, roll));
}
public static bool ShouldCreateLight(bool isOn, bool hasValidLight)
{
return isOn && !hasValidLight;
}
public static bool ShouldDestroyLight(bool isOn, bool hasValidLight)
{
return !isOn && hasValidLight;
}
/// <summary>
/// Returns whether <paramref name="team"/> may use the flashlight.
/// Team values match CS2: 2 = Terrorist, 3 = Counter-Terrorist.
/// </summary>
public static bool IsTeamAllowed(string allowedTeam, byte team)
{
return allowedTeam switch
{
"T" => team == 2,
"CT" => team == 3,
_ => true
};
}
}