mirror of
https://github.com/creazy231/cs2-css-flashlight.git
synced 2026-09-27 20:17:04 +02:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b43f9fdea3 |
5 changed files with 217 additions and 48 deletions
|
|
@ -70,6 +70,79 @@ public class FlashlightLogicTests
|
|||
Assert.Equal(0f, forward.Z, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForwardFromAnglesDegrees_PitchDrivesVerticalComponent()
|
||||
{
|
||||
// Source angles are inverted on pitch: positive pitch looks down.
|
||||
Assert.True(FlashlightLogic.ForwardFromAnglesDegrees(45f, 0f).Z < 0f);
|
||||
Assert.True(FlashlightLogic.ForwardFromAnglesDegrees(-45f, 0f).Z > 0f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalForwardFromYawDegrees_HasNoVerticalComponent()
|
||||
{
|
||||
foreach (var yaw in new[] { -180f, -90f, -45f, 0f, 45f, 90f, 180f })
|
||||
{
|
||||
Assert.Equal(0f, FlashlightLogic.HorizontalForwardFromYawDegrees(yaw).Z, 5);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLightTransform_AnglesCarryFullViewRotation()
|
||||
{
|
||||
var transform = FlashlightLogic.CalculateLightTransform(
|
||||
pawnOrigin: new Vector3(0f, 0f, 0f),
|
||||
pitch: -35f,
|
||||
yaw: 90f,
|
||||
roll: 12f,
|
||||
eyeOffsetZ: 64f,
|
||||
forwardDistance: 54f);
|
||||
|
||||
// Pitch must survive into the light's angles, otherwise the beam only tracks yaw.
|
||||
Assert.Equal(-35f, transform.Angles.X, 3);
|
||||
Assert.Equal(90f, transform.Angles.Y, 3);
|
||||
Assert.Equal(12f, transform.Angles.Z, 3);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-89f)]
|
||||
[InlineData(-45f)]
|
||||
[InlineData(0f)]
|
||||
[InlineData(45f)]
|
||||
[InlineData(89f)]
|
||||
public void CalculateLightTransform_PitchNeverMovesTheOrigin(float pitch)
|
||||
{
|
||||
var transform = FlashlightLogic.CalculateLightTransform(
|
||||
new Vector3(0f, 0f, 0f),
|
||||
pitch,
|
||||
yaw: 0f,
|
||||
roll: 0f,
|
||||
eyeOffsetZ: 64f,
|
||||
forwardDistance: 54f);
|
||||
|
||||
// The muzzle stays at eye height and eye-forward regardless of pitch, so
|
||||
// looking up or down can never shove the light through the ceiling or floor.
|
||||
Assert.Equal(54f, transform.Origin.X, 3);
|
||||
Assert.Equal(0f, transform.Origin.Y, 3);
|
||||
Assert.Equal(64f, transform.Origin.Z, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLightTransform_YawDrivesHorizontalOffset()
|
||||
{
|
||||
var transform = FlashlightLogic.CalculateLightTransform(
|
||||
new Vector3(10f, 20f, 30f),
|
||||
pitch: 0f,
|
||||
yaw: 90f,
|
||||
roll: 0f,
|
||||
eyeOffsetZ: 64f,
|
||||
forwardDistance: 54f);
|
||||
|
||||
Assert.Equal(10f, transform.Origin.X, 3);
|
||||
Assert.Equal(74f, transform.Origin.Y, 3);
|
||||
Assert.Equal(94f, transform.Origin.Z, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShouldCreateAndDestroyLight_Policies()
|
||||
{
|
||||
|
|
@ -98,7 +171,6 @@ public class FlashlightLogicTests
|
|||
ForwardDistance = -10f,
|
||||
StandEyeOffsetZ = -1f,
|
||||
CrouchEyeOffsetZ = -1f,
|
||||
AttachmentName = " ",
|
||||
LightCookie = ""
|
||||
};
|
||||
|
||||
|
|
@ -118,7 +190,6 @@ public class FlashlightLogicTests
|
|||
Assert.Equal(0f, config.ForwardDistance);
|
||||
Assert.Equal(0f, config.StandEyeOffsetZ);
|
||||
Assert.Equal(0f, config.CrouchEyeOffsetZ);
|
||||
Assert.Equal("axis_of_intent", config.AttachmentName);
|
||||
Assert.Equal("materials/effects/lightcookies/flashlight.vtex", config.LightCookie);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -1,12 +1,12 @@
|
|||
# Flashlight
|
||||
|
||||
Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrikeSharp](https://docs.cssharp.dev). It gives human players a toggleable flashlight using a parented `light_barn` entity.
|
||||
Flashlight is a Counter-Strike 2 server plugin written in C# with [CounterStrikeSharp](https://docs.cssharp.dev). It gives human players a toggleable flashlight using a `light_barn` entity.
|
||||
|
||||
## Features
|
||||
|
||||
- Toggle with the Use key (`E` by default) or `css_fl_toggle`
|
||||
- One `light_barn` per player, parented to the pawn attachment (no per-tick spawn/teleport)
|
||||
- Configurable brightness, range, color, shadows, offsets, and attachment
|
||||
- One `light_barn` per player, re-aimed each tick so the beam tracks both pitch and yaw
|
||||
- Configurable brightness, range, color, shadows, and offsets
|
||||
- Optional team restriction (`Any`, `CT`, or `T`)
|
||||
- Automatically turns off on death, spawn, and team change
|
||||
- Bots ignored
|
||||
|
|
@ -49,10 +49,9 @@ On first load, CounterStrikeSharp writes a JSON config for the plugin. Defaults:
|
|||
| `SoftX` / `SoftY` | `1.0` | Softness |
|
||||
| `Skirt` / `SkirtNear` | `0.5` / `1.0` | Skirt falloff |
|
||||
| `SizeX` / `SizeY` / `SizeZ` | `45` / `45` / `0.03` | Beam size params |
|
||||
| `ForwardDistance` | `54` | Spawn offset along view forward |
|
||||
| `ForwardDistance` | `54` | Horizontal offset in front of the eye, so the beam clears the player model |
|
||||
| `StandEyeOffsetZ` | `64` | Standing eye height offset |
|
||||
| `CrouchEyeOffsetZ` | `46` | Crouching eye height offset |
|
||||
| `AttachmentName` | `axis_of_intent` | Parent attachment |
|
||||
| `LightCookie` | `materials/effects/lightcookies/flashlight.vtex` | Flashlight cookie texture |
|
||||
|
||||
## Development
|
||||
|
|
@ -75,10 +74,17 @@ dotnet build
|
|||
dotnet test
|
||||
```
|
||||
|
||||
Unit tests cover toggle/cooldown logic, Use-key edge detection, origin math, create/destroy policy, and config clamping. Entity parenting requires a live CS2 server.
|
||||
Unit tests cover toggle/cooldown logic, Use-key edge detection, transform math (origin and pitch/yaw angles), create/destroy policy, and config clamping. Entity behaviour itself requires a live CS2 server.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.1.2
|
||||
|
||||
- Fixed the beam only following horizontal aim: the light was parented to the pawn's `axis_of_intent` attachment, which carries body yaw but not view pitch, so looking up or down never moved it. The light is now un-parented and re-aimed every tick from the pawn's live `V_angle`.
|
||||
- Fixed the flashlight never updating when `AllowUseKey` was `false`, which previously short-circuited the whole tick loop.
|
||||
- `ForwardDistance` now offsets the light horizontally only, so looking straight down no longer pushes it through the floor.
|
||||
- Removed the obsolete `AttachmentName` config key (leaving it in an existing config file is harmless and ignored).
|
||||
|
||||
### v0.1.1
|
||||
|
||||
- Updated to .NET 10 and CounterStrikeSharp.API 1.0.371
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue