mirror of
https://github.com/creazy231/cs2-css-flashlight.git
synced 2026-09-27 20:17:04 +02:00
🔦 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>
This commit is contained in:
parent
b3bbc2dc9a
commit
b43f9fdea3
5 changed files with 217 additions and 48 deletions
|
|
@ -16,7 +16,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
public override string ModuleAuthor => "creazy.eth";
|
||||
public override string ModuleName => "Flashlight";
|
||||
public override string ModuleDescription => "Flashlight for Counter-Strike 2";
|
||||
public override string ModuleVersion => "0.1.1";
|
||||
public override string ModuleVersion => "0.1.2";
|
||||
|
||||
public FlashlightConfig Config { get; set; } = new();
|
||||
|
||||
|
|
@ -57,27 +57,49 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
|
||||
private void OnTick()
|
||||
{
|
||||
if (!Config.Enabled || !Config.AllowUseKey)
|
||||
if (!Config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
{
|
||||
if (!IsEligiblePlayer(player) || !player.PawnIsAlive)
|
||||
if (!IsEligiblePlayer(player))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = EnsureState(player);
|
||||
var usePressed = (player.Buttons & PlayerButtons.Use) != 0;
|
||||
|
||||
if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed))
|
||||
if (!player.PawnIsAlive)
|
||||
{
|
||||
TryToggleFlashlight(player, state);
|
||||
// An un-parented light no longer dies together with the pawn, so sweep it up here
|
||||
// in case a death or round-end never reached the event handlers.
|
||||
if (_playerStates.TryGetValue(player.Slot, out var deadState) && deadState.IsOn)
|
||||
{
|
||||
deadState.IsOn = false;
|
||||
DestroyLight(player.Slot);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
state.WasUsePressed = usePressed;
|
||||
var state = EnsureState(player);
|
||||
|
||||
if (Config.AllowUseKey)
|
||||
{
|
||||
var usePressed = (player.Buttons & PlayerButtons.Use) != 0;
|
||||
|
||||
if (FlashlightLogic.IsUsePressedEdge(usePressed, state.WasUsePressed))
|
||||
{
|
||||
TryToggleFlashlight(player, state);
|
||||
}
|
||||
|
||||
state.WasUsePressed = usePressed;
|
||||
}
|
||||
|
||||
if (state.IsOn)
|
||||
{
|
||||
UpdateLight(player, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +210,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
|
||||
if (state.IsOn)
|
||||
{
|
||||
CreateAndParentLight(player, state);
|
||||
CreateLight(player, state);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -205,7 +227,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
});
|
||||
}
|
||||
|
||||
private void CreateAndParentLight(CCSPlayerController player, PlayerFlashlightState state)
|
||||
private void CreateLight(CCSPlayerController player, PlayerFlashlightState state)
|
||||
{
|
||||
DestroyLight(player.Slot);
|
||||
|
||||
|
|
@ -225,20 +247,6 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
return;
|
||||
}
|
||||
|
||||
var isCrouching = (player.Buttons & PlayerButtons.Duck) != 0;
|
||||
var eyeOffsetZ = FlashlightLogic.GetEyeOffsetZ(
|
||||
isCrouching,
|
||||
Config.StandEyeOffsetZ,
|
||||
Config.CrouchEyeOffsetZ);
|
||||
|
||||
var angles = pawn.V_angle;
|
||||
var forward = FlashlightLogic.ForwardFromAnglesDegrees(angles.X, angles.Y);
|
||||
var origin = FlashlightLogic.CalculateLightOrigin(
|
||||
new Vector3(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z),
|
||||
forward,
|
||||
eyeOffsetZ,
|
||||
Config.ForwardDistance);
|
||||
|
||||
light.Enabled = true;
|
||||
light.Color = Color.FromArgb(255, Config.ColorR, Config.ColorG, Config.ColorB);
|
||||
light.ColorTemperature = Config.ColorTemperature;
|
||||
|
|
@ -254,10 +262,7 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
light.SizeParams.Y = Config.SizeY;
|
||||
light.SizeParams.Z = Config.SizeZ;
|
||||
|
||||
light.Teleport(
|
||||
origin,
|
||||
new Vector3(angles.X, angles.Y, angles.Z),
|
||||
null);
|
||||
ApplyTransform(light, player, pawn);
|
||||
|
||||
using (var keyValues = new CEntityKeyValues())
|
||||
{
|
||||
|
|
@ -265,13 +270,69 @@ public class FlashlightPlugin : BasePlugin, IPluginConfig<FlashlightConfig>
|
|||
light.DispatchSpawn(keyValues);
|
||||
}
|
||||
|
||||
light.AcceptInput("SetParent", pawn, light, "!activator");
|
||||
light.AcceptInput("SetParentAttachmentMaintainOffset", null, null, Config.AttachmentName);
|
||||
|
||||
state.Light = light;
|
||||
state.IsOn = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the light glued to the player's eye every tick.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The light is deliberately not parented to the pawn. Handing it to the engine via
|
||||
/// <c>SetParent</c> / <c>SetParentAttachmentMaintainOffset</c> locks its orientation to a model
|
||||
/// attachment, and those attachments only carry the body's yaw, so the beam could never follow
|
||||
/// the player looking up or down. Driving the transform ourselves keeps pitch and yaw in sync.
|
||||
/// </remarks>
|
||||
private void UpdateLight(CCSPlayerController player, PlayerFlashlightState state)
|
||||
{
|
||||
var light = state.Light;
|
||||
|
||||
if (light is null || !light.IsValid)
|
||||
{
|
||||
// The engine can reap the entity underneath us (round restart, cleanup); rebuild it.
|
||||
state.Light = null;
|
||||
CreateLight(player, state);
|
||||
return;
|
||||
}
|
||||
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn is null || !pawn.IsValid || pawn.AbsOrigin is null || pawn.V_angle is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTransform(light, player, pawn);
|
||||
}
|
||||
|
||||
private void ApplyTransform(CBarnLight light, CCSPlayerController player, CCSPlayerPawn pawn)
|
||||
{
|
||||
var isCrouching = (player.Buttons & PlayerButtons.Duck) != 0;
|
||||
var eyeOffsetZ = FlashlightLogic.GetEyeOffsetZ(
|
||||
isCrouching,
|
||||
Config.StandEyeOffsetZ,
|
||||
Config.CrouchEyeOffsetZ);
|
||||
|
||||
var origin = pawn.AbsOrigin!;
|
||||
var angles = pawn.V_angle;
|
||||
|
||||
var transform = FlashlightLogic.CalculateLightTransform(
|
||||
new Vector3(origin.X, origin.Y, origin.Z),
|
||||
angles.X,
|
||||
angles.Y,
|
||||
angles.Z,
|
||||
eyeOffsetZ,
|
||||
Config.ForwardDistance);
|
||||
|
||||
// Handing the pawn's velocity over lets clients interpolate the light between ticks
|
||||
// instead of visibly stepping it.
|
||||
var velocity = pawn.AbsVelocity;
|
||||
|
||||
light.Teleport(
|
||||
transform.Origin,
|
||||
transform.Angles,
|
||||
velocity is null ? null : new Vector3(velocity.X, velocity.Y, velocity.Z));
|
||||
}
|
||||
|
||||
private void DestroyLight(int slot)
|
||||
{
|
||||
if (!_playerStates.TryGetValue(slot, out var state))
|
||||
|
|
|
|||
|
|
@ -71,9 +71,6 @@ public class FlashlightConfig : BasePluginConfig
|
|||
[JsonPropertyName("CrouchEyeOffsetZ")]
|
||||
public float CrouchEyeOffsetZ { get; set; } = 46f;
|
||||
|
||||
[JsonPropertyName("AttachmentName")]
|
||||
public string AttachmentName { get; set; } = "axis_of_intent";
|
||||
|
||||
[JsonPropertyName("LightCookie")]
|
||||
public string LightCookie { get; set; } = "materials/effects/lightcookies/flashlight.vtex";
|
||||
|
||||
|
|
@ -94,11 +91,6 @@ public class FlashlightConfig : BasePluginConfig
|
|||
StandEyeOffsetZ = Math.Max(0f, StandEyeOffsetZ);
|
||||
CrouchEyeOffsetZ = Math.Max(0f, CrouchEyeOffsetZ);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(AttachmentName))
|
||||
{
|
||||
AttachmentName = "axis_of_intent";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(LightCookie))
|
||||
{
|
||||
LightCookie = "materials/effects/lightcookies/flashlight.vtex";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ 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)
|
||||
|
|
@ -50,6 +56,39 @@ public static class FlashlightLogic
|
|||
-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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue