Compare commits

...

10 Commits

Author SHA1 Message Date
MSWS
5736588484 refactor: Rename Id to WeaponId across the codebase
- Rename property `Id` to `WeaponId` in `IWeapon.cs`, `BaseWeapon.cs`, and `OneShotDeagle.cs` for improved clarity.
- Update weapon removal method in `IInventoryManager.cs` to use `weapon.WeaponId`.
- Refactor `PlayerDamagedEvent.cs` to initialize `Weapon` property with `init` for stricter immutability.
- Revise `IsAlive` logic in `TestPlayer.cs` to adjust `Health` based on `IsAlive` status; deprecate the `Roles` property.
- Add `using` directive and `[UsedImplicitly]` attribute to `DeagleDamageListener.cs` for dependency management and traceability.
- Develop `DeagleTests.cs` to ensure proper functionality of Deagle weapon behaviors using Xunit.
2025-09-28 00:16:28 -07:00
MSWS
8a894c65e8 refactor: Adjust player color handling logic
- Adjust the alpha value handling in `SetColor` method within `PlayerExtensions.cs` to ensure it stays within limits.
- Simplify player color setting in `RoundTimerListener.cs` by using `Color.White` for improved readability.
- Update `BodySpawner.cs` to change player post-death color to fully opaque white and simplify round start color setting using `Color.White`.
2025-09-27 20:30:07 -07:00
MSWS
0634af8ad8 feat: Add weapon slot removal functionality
- Fix typo in comment and clarify kill detection process in `PlayerStatsTracker.cs`
- Add `RemoveWeaponInSlot` method for slot-based weapon removal in `FakeInventoryManager.cs` and update `IInventoryManager.cs` for enhanced functionality
- Reformat `BaseWeapon` constructor for readability and standardize file formatting
- Simplify `CS2Body.cs` API by removing overloaded weapon method and reinforce `IWeapon` interface usage
- Refactor `CS2InventoryManager.cs` for improved weapon management, add methods for slot conversion and weapon removal, and streamline code structure
- Add debug messaging to `DeagleDamageListener.cs` for better runtime clarity on friendly fire and one-shot kill conditions
- Modify `BodySpawner.cs` to incorporate `BaseWeapon` wrapper for improved weapon management without affecting core functionality
- Adjust `OnPlayerKill` event handler priority in `PlayerActionsLogger.cs` to ensure kill logging before game ends
2025-09-27 20:25:53 -07:00
MSWS
9f6c3f7be4 Check player count on round start before starting 2025-09-27 19:07:28 -07:00
MSWS
9eb313e9f1 feat: Introduce screen color features and state command
```
Enhance game management and command structure with new features and optimizations

- Add logic in `PlayerJoinStarting.cs` to handle failure in game instance creation and ensure game starts only upon successful creation.
- Introduce IMessenger service in `CS2GameManager.cs` using dependency injection; add debug logging for the game creation process.
- Implement new behavior for `ScreenColorApplier` in `CS2ServiceCollection.cs` to enhance listening capabilities.
- Create a new `StateCommand` in `StateCommand.cs` to check active game state and implement basic command lifecycle methods.
- Remove "screentext" command and add "state" and "screencolor" commands in `TestCommand.cs` to revamp command options.
- Simplify vector operations in `TextSpawner.cs` by replacing `GetRightVector`; enhance screen text positioning.
- Expand `VectorExtensions.cs` with `ToRight` and `ToUp` methods for better angle-to-vector conversions.
- Remove `ScreenTextCommand.cs` to streamline project and potentially refactor screen text feature.
- Add `ScreenColorApplier` to apply screen color effects upon role assignment, enhancing player interaction experience.
- Introduce `ScreenColorCommand` for player screen color effects with configurable parameters, enriching command functionality.
```
2025-09-26 20:28:35 -07:00
MSWS
9b5563aa8e feat: Refactor text positioning and add screen fade effects
- Refactor `TextSpawner.cs` to improve angle calculations and text positioning with respect to the player.
- Replace static `screenAngle` with local computations for better clarity and maintainability in `TextSpawner.cs`.
- Introduce `angle` object in `TextSpawner.cs` to enhance readability and explicitness in rotation management.
- Add `FadeFlags` enum and implement `ColorScreen` method in `PlayerExtensions.cs` to manage screen color fade effects.
- Enhance color fade handling in `PlayerExtensions.cs` by utilizing `UserMessage` with custom flag settings and improved color configurations.
2025-09-26 18:47:18 -07:00
MSWS
2058d0c780 Start work on screen text 2025-09-26 18:29:59 -07:00
Isaac
ee7f34b435 Bump the nuget group with 1 update (#58)
Pinned System.Text.Json at 8.0.5.

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/MSWS/TTT/network/alerts).

</details>
2025-09-26 18:00:40 -07:00
Isaac
783f6da6dd Merge branch 'main' into dependabot/nuget/Locale/nuget-9df58bc52d 2025-09-26 17:58:41 -07:00
dependabot[bot]
fca81c0577 Bump the nuget group with 1 update
Bumps System.Text.Json from 8.0.0 to 8.0.5

---
updated-dependencies:
- dependency-name: System.Text.Json
  dependency-version: 8.0.5
  dependency-type: direct:production
  dependency-group: nuget
- dependency-name: System.Text.Json
  dependency-version: 8.0.5
  dependency-type: direct:production
  dependency-group: nuget
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-26 04:40:30 +00:00
29 changed files with 386 additions and 40 deletions

View File

@@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.332"/>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.3"/>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="YamlDotNet" Version="16.3.0"/>
</ItemGroup>

View File

@@ -5,4 +5,7 @@
<Nullable>enable</Nullable>
<RootNamespace>TTT.API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>

View File

@@ -4,7 +4,7 @@ public interface IWeapon {
/// <summary>
/// The internal ID of the weapon, should match the ID of the weapon in the underlying game.
/// </summary>
public string Id { get; }
public string WeaponId { get; }
/// <summary>
/// The amount of ammo that is in reserve for this weapon.

View File

@@ -16,9 +16,10 @@ public interface IInventoryManager {
void RemoveWeapon(IOnlinePlayer player, string weaponId);
void RemoveWeapon(IOnlinePlayer player, IWeapon weapon) {
RemoveWeapon(player, weapon.Id);
RemoveWeapon(player, weapon.WeaponId);
}
void RemoveWeaponInSlot(IOnlinePlayer player, int slot);
/// <summary>
/// Removes all weapons from the player.

View File

@@ -37,10 +37,6 @@ public class CS2Body(IServiceProvider provider, CRagdollProp ragdoll,
return this;
}
public CS2Body WithWeapon(string weapon) {
return WithWeapon(new BaseWeapon(weapon));
}
public CS2Body WithKiller(IPlayer? killer) {
Killer = killer;
return this;

View File

@@ -63,9 +63,12 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<LateSpawnListener>();
collection.AddModBehavior<PlayerStatsTracker>();
collection.AddModBehavior<RoundTimerListener>();
collection.AddModBehavior<ScreenColorApplier>();
// Commands
#if DEBUG
collection.AddModBehavior<TestCommand>();
#endif
collection.AddScoped<IGameManager, CS2GameManager>();
collection.AddScoped<IInventoryManager, CS2InventoryManager>();

View File

@@ -0,0 +1,35 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.Extensions;
namespace TTT.CS2.Command.Test;
public class ScreenColorCommand(IServiceProvider provider) : ICommand {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public string Name => "screencolor";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
Server.NextWorldUpdate(() => {
var player = converter.GetPlayer(executor);
float hold = 0.5f, fade = 0.5f;
if (info.ArgCount >= 2) float.TryParse(info.Args[1], out hold);
if (info.ArgCount >= 3) float.TryParse(info.Args[2], out fade);
player?.ColorScreen(Color.Red, hold, fade);
info.ReplySync("Colored your screen red.");
});
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class StateCommand(IServiceProvider provider) : ICommand {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
public string Name => "state";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (games.ActiveGame == null) {
info.ReplySync("ActiveGame is null.");
return Task.FromResult(CommandResult.SUCCESS);
}
info.ReplySync($"Current game state: {games.ActiveGame?.State}");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -18,6 +18,8 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("stop", new StopCommand(provider));
subCommands.Add("forcealive", new ForceAliveCommand(provider));
subCommands.Add("identifyall", new IdentifyAllCommand(provider));
subCommands.Add("state", new StateCommand(provider));
subCommands.Add("screencolor", new ScreenColorCommand(provider));
}
public Task<CommandResult>

View File

@@ -1,6 +1,7 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
namespace TTT.CS2.Extensions;
@@ -31,7 +32,35 @@ public static class PlayerExtensions {
if (!player.IsValid || pawn == null || !pawn.IsValid) return;
if (color.A == 255)
color = Color.FromArgb(pawn.Render.A, color.R, color.G, color.B);
color = Color.FromArgb(pawn.Render.A == 255 ? 255 : 254, color.R, color.G,
color.B);
pawn.SetColor(color);
}
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
public static void ColorScreen(this CCSPlayerController player, Color color,
float hold = 0.1f, float fade = 0.2f, FadeFlags flags = FadeFlags.FADE_IN,
bool withPurge = true) {
var fadeMsg = UserMessage.FromId(106);
fadeMsg.SetInt("duration", Convert.ToInt32(fade * 512));
fadeMsg.SetInt("hold_time", Convert.ToInt32(hold * 512));
var flag = flags switch {
FadeFlags.FADE_IN => 0x0001,
FadeFlags.FADE_OUT => 0x0002,
FadeFlags.FADE_STAYOUT => 0x0008,
_ => 0x0001
};
if (withPurge) flag |= 0x0010;
fadeMsg.SetInt("flags", flag);
fadeMsg.SetInt("color",
color.R | color.G << 8 | color.B << 16 | color.A << 24);
fadeMsg.Send(player);
}
}

View File

@@ -46,6 +46,40 @@ public static class VectorExtensions {
(float)(Math.Sin(yaw) * cosPitch), (float)-Math.Sin(pitch));
}
public static Vector ToRight(this QAngle angle) {
var pitch = angle.X * (Math.PI / 180.0);
var yaw = angle.Y * (Math.PI / 180.0);
var roll = angle.Z * (Math.PI / 180.0);
var sinPitch = Math.Sin(pitch);
var cosPitch = Math.Cos(pitch);
var sinYaw = Math.Sin(yaw);
var cosYaw = Math.Cos(yaw);
var sinRoll = Math.Sin(roll);
var cosRoll = Math.Cos(roll);
return new Vector((float)(sinYaw * sinPitch * cosRoll - cosYaw * sinRoll),
(float)(-cosYaw * sinPitch * cosRoll - sinYaw * sinRoll),
(float)(cosPitch * -sinRoll));
}
public static Vector ToUp(this QAngle angle) {
var pitch = angle.X * (Math.PI / 180.0);
var yaw = angle.Y * (Math.PI / 180.0);
var roll = angle.Z * (Math.PI / 180.0);
var sinPitch = Math.Sin(pitch);
var cosPitch = Math.Cos(pitch);
var sinYaw = Math.Sin(yaw);
var cosYaw = Math.Cos(yaw);
var sinRoll = Math.Sin(roll);
var cosRoll = Math.Cos(roll);
return new Vector((float)(-cosYaw * sinPitch * cosRoll - sinYaw * sinRoll),
(float)(-sinYaw * sinPitch * cosRoll + cosYaw * sinRoll),
(float)(cosPitch * cosRoll));
}
public static Vector Lerp(this Vector from, Vector to, float t) {
return new Vector(from.X + (to.X - from.X) * t,
from.Y + (to.Y - from.Y) * t, from.Z + (to.Z - from.Z) * t);

View File

@@ -1,11 +1,17 @@
using TTT.API.Game;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.Game;
using TTT.Game.Events.Game;
namespace TTT.CS2.Game;
public class CS2GameManager(IServiceProvider provider) : GameManager(provider) {
protected readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public override IGame CreateGame() {
messenger.Debug($"Attempting to create a new CS2 game...");
switch (ActiveGame) {
case { State: State.IN_PROGRESS or State.COUNTDOWN }:
throw new InvalidOperationException(
@@ -14,6 +20,7 @@ public class CS2GameManager(IServiceProvider provider) : GameManager(provider) {
return ActiveGame;
}
messenger.Debug("Creating a new CS2 game instance...");
ActiveGame = new CS2Game(Provider);
var ev = new GameInitEvent(ActiveGame);

View File

@@ -11,6 +11,7 @@ using TTT.API.Game;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
@@ -32,7 +33,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
var player = ev.Userid;
if (player == null || !player.IsValid) return HookResult.Continue;
player.SetColor(Color.FromArgb(0, 0, 0, 0));
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(provider, ragdollBody, converter.GetPlayer(player));
@@ -40,7 +41,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
body.WithWeapon(ev.Weapon);
body.WithWeapon(new BaseWeapon(ev.Weapon));
var bodyCreatedEvent = new BodyCreateEvent(body);
bus.Dispatch(bodyCreatedEvent);
@@ -53,7 +54,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
public HookResult OnStart(EventRoundStart ev, GameEventInfo _) {
Server.NextWorldUpdate(() => {
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
player.SetColor(Color.White);
});
return HookResult.Continue;
}

View File

@@ -4,6 +4,7 @@ using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game;
@@ -15,6 +16,9 @@ public class RoundStart_GameStartHandler(IServiceProvider provider)
provider.GetService<IStorage<TTTConfig>>()?.Load().GetAwaiter().GetResult()
?? new TTTConfig();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
@@ -28,6 +32,9 @@ public class RoundStart_GameStartHandler(IServiceProvider provider)
if (games.ActiveGame is { State: State.IN_PROGRESS or State.COUNTDOWN })
return HookResult.Continue;
var count = finder.GetOnline().Count;
if (count < config.RoundCfg.MinimumPlayers) return HookResult.Continue;
var game = games.CreateGame();
game?.Start(config.RoundCfg.CountDownDuration);
return HookResult.Continue;

View File

@@ -19,4 +19,7 @@ public interface ITextSpawner {
IEnumerable<CPointWorldText> CreateTextHat(TextSetting setting,
CCSPlayerController player);
IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player);
}

View File

@@ -2,6 +2,7 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using Vector = CounterStrikeSharp.API.Modules.Utils.Vector;
namespace TTT.CS2.Hats;
@@ -36,6 +37,31 @@ public class TextSpawner : ITextSpawner {
return [one, two];
}
public IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player) {
var screen = spawnScreen(setting, player);
return [screen];
}
public CPointWorldText spawnScreen(TextSetting setting,
CCSPlayerController player, float xOff = 0, float yOff = 0,
float zDist = 50) {
if (player.Pawn.Value == null || player.Pawn.Value.AbsRotation == null)
throw new Exception("Failed to get player rotation");
var eyes = player.GetEyePosition().Clone()!;
var localAngle = player.Pawn.Value.AbsRotation.Clone()!;
var forward = localAngle.Clone()!.ToForward();
var right = localAngle.ToRight();
var up = localAngle.ToUp();
var inFront = eyes + forward * zDist;
var centered = inFront + right * xOff + up * yOff;
var ent = CreateText(setting, centered,
new QAngle(localAngle.X + 180, localAngle.Y + 90, localAngle.Z + 270));
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");
return ent;
}
private CPointWorldText spawnHatPart(TextSetting setting,
CCSPlayerController player, float yRot) {
var position = player.Pawn.Value?.AbsOrigin;
@@ -46,19 +72,10 @@ public class TextSpawner : ITextSpawner {
position.Add(new Vector(0, 0, 72));
rotation = new QAngle(rotation.X, rotation.Y + yRot, rotation.Z + 90);
position.Add(GetRightVector(rotation) * -10);
position.Add(rotation.ToRight() * -10);
var ent = CreateText(setting, position, rotation);
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");
return ent;
}
public static Vector GetRightVector(QAngle rotation) {
var forward = new Vector {
X = (float)Math.Cos(rotation.Y * Math.PI / 180),
Y = (float)Math.Sin(rotation.Y * Math.PI / 180),
Z = 0
};
return forward;
}
}

View File

@@ -42,7 +42,7 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
revealedDeaths.Add(gamePlayer.Slot);
}
// Needs to be higher so we detect the kill the game ends
// Needs to be higher so we detect the kill before the game ends
// in the case that this is the last player
[EventHandler(Priority = Priority.HIGHER)]
public void OnKill(PlayerDeathEvent ev) {

View File

@@ -43,7 +43,7 @@ public class RoundTimerListener(IServiceProvider provider)
player.Respawn();
foreach (var player in Utilities.GetPlayers())
player.SetColor(Color.FromArgb(254, 255, 255, 255));
player.SetColor(Color.White);
});
return;

View File

@@ -0,0 +1,30 @@
using System.Drawing;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.CS2.Roles;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.CS2.Listeners;
public class ScreenColorApplier(IServiceProvider provider)
: BaseListener(provider) {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
if (ev.Role is SpectatorRole) return;
var player = converter.GetPlayer(ev.Player);
var alphaColor = Color.FromArgb(16, ev.Role.Color);
if (player != null)
player.ColorScreen(alphaColor, 5f, 5f,
flags: PlayerExtensions.FadeFlags.FADE_OUT);
player?.PrintToCenterHtml("You are a " + ev.Role.Name, 20);
}
}

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using TTT.API;
using TTT.API.Player;
using TTT.CS2.Extensions;
@@ -13,17 +14,67 @@ public class CS2InventoryManager(
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
gamePlayer.GiveNamedItem(weapon.Id);
if (weapon.ReserveAmmo == null && weapon.CurrentAmmo == null) return;
var weaponBase = gamePlayer.GetWeaponBase(weapon.Id);
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null)
weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null)
weaponBase.Clip2 = weapon.ReserveAmmo.Value;
giveWeapon(gamePlayer, weapon);
});
}
private void giveWeapon(CCSPlayerController player, IWeapon weapon) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null) weaponBase.Clip2 = weapon.ReserveAmmo.Value;
}
public static gear_slot_t IntToSlot(int slot)
=> slot switch {
0 => gear_slot_t.GEAR_SLOT_RIFLE,
1 => gear_slot_t.GEAR_SLOT_PISTOL,
2 => gear_slot_t.GEAR_SLOT_KNIFE,
3 => gear_slot_t.GEAR_SLOT_UTILITY,
4 => gear_slot_t.GEAR_SLOT_C4,
_ => gear_slot_t.GEAR_SLOT_FIRST
};
public static int SlotToInt(gear_slot_t slot)
=> slot switch {
gear_slot_t.GEAR_SLOT_RIFLE => 0,
gear_slot_t.GEAR_SLOT_PISTOL => 1,
gear_slot_t.GEAR_SLOT_KNIFE => 2,
gear_slot_t.GEAR_SLOT_UTILITY => 3,
gear_slot_t.GEAR_SLOT_C4 => 4,
_ => -1
};
private void clearSlot(CCSPlayerController player,
params gear_slot_t[] slots) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
var weapons = player.Pawn.Value?.WeaponServices?.MyWeapons;
if (weapons == null || weapons.Count == 0) return;
foreach (var weapon in weapons) {
if (!weapon.IsValid || weapon.Value == null) continue;
if (!weapon.Value.IsValid
|| !weapon.Value.DesignerName.StartsWith("weapon_"))
continue;
if (weapon.Value.Entity == null) continue;
if (!weapon.Value.OwnerEntity.IsValid) continue;
var weaponBase = weapon.Value.As<CBaseEntity>();
if (!weaponBase.IsValid || (weaponBase.Entity == null)) continue;
var weaponData = (weaponBase as CCSWeaponBase)?.VData;
if (weaponData == null) continue;
if (!slots.Contains(weaponData.GearSlot)) continue;
weapon.Value.AddEntityIOEvent("Kill", weapon.Value);
}
}
public void RemoveWeapon(IOnlinePlayer player, string weaponId) {
Server.NextWorldUpdate(() => {
if (!player.IsAlive) return;
@@ -51,6 +102,17 @@ public class CS2InventoryManager(
});
}
public void RemoveWeaponInSlot(IOnlinePlayer player, int slot) {
Server.NextWorldUpdate(() => {
if (!player.IsAlive) return;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null) return;
clearSlot(gamePlayer, IntToSlot(slot));
});
}
public void RemoveAllWeapons(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
if (!player.IsAlive) return;

View File

@@ -80,6 +80,6 @@ public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
}
}
public string? Weapon { get; private set; }
public string? Weapon { get; init; }
public bool IsCanceled { get; set; }
}

View File

@@ -8,7 +8,8 @@ namespace TTT.Game.Listeners.Loggers;
public class PlayerActionsLogger(IServiceProvider provider)
: BaseListener(provider) {
[EventHandler]
// Needs to be higher so we detect the kill before the game ends
[EventHandler(Priority = Priority.HIGHER)]
[UsedImplicitly]
public void OnPlayerKill(PlayerDeathEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;

View File

@@ -25,6 +25,9 @@ public class PlayerJoinStarting(IServiceProvider provider)
$"There are {playerCount} Players online, starting the game...");
var game = Games.CreateGame();
if (game == null)
Messenger.DebugAnnounce("Failed to create a new game instance.");
game?.Start(config.RoundCfg.CountDownDuration);
}
}

View File

@@ -2,9 +2,9 @@
namespace TTT.Game.Roles;
public class BaseWeapon(string id, int? reserve = null, int? current = null)
: IWeapon {
public string Id { get; } = id;
public class BaseWeapon(string id, int? reserve = null,
int? current = null) : IWeapon {
public string WeaponId { get; } = id;
public int? ReserveAmmo { get; } = reserve;
public int? CurrentAmmo { get; } = current;
}

View File

@@ -1,3 +1,4 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
@@ -18,6 +19,7 @@ public class DeagleDamageListener(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnDamage(PlayerDamagedEvent ev) {
Messenger.Debug("DeagleDamageListener: OnDamage");
@@ -42,10 +44,16 @@ public class DeagleDamageListener(IServiceProvider provider)
var victimRole = Roles.GetRoles(victim);
shop.RemoveItem(attacker, deagleItem);
if (!config.DoesFriendlyFire && attackerRole.Intersect(victimRole).Any())
if (!config.DoesFriendlyFire && attackerRole.Intersect(victimRole).Any()) {
Messenger.DebugAnnounce(
"DeagleDamageListener: Friendly fire is off, roles intersect");
return;
}
Messenger.DebugAnnounce(
"DeagleDamageListener: One-shot kill conditions met");
if (victim is not IOnlinePlayer onlineVictim) return;
Messenger.DebugAnnounce("DeagleDamageListener: One-shot kill applied");
onlineVictim.Health = 0;
}
}

View File

@@ -47,7 +47,7 @@ public class OneShotDeagle(IServiceProvider provider) : IWeapon, IShopItem {
string IShopItem.Id => ID;
public string Id => deagleConfigStorage.Weapon;
public string WeaponId => deagleConfigStorage.Weapon;
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;

View File

@@ -6,6 +6,6 @@ namespace TTT.Test.Fakes;
public class FakeInventoryManager : IInventoryManager {
public void GiveWeapon(IOnlinePlayer player, IWeapon weapon) { }
public void RemoveWeapon(IOnlinePlayer player, string weaponId) { }
public void RemoveWeaponInSlot(IOnlinePlayer player, int slot) { }
public void RemoveAllWeapons(IOnlinePlayer player) { }
}

View File

@@ -0,0 +1,69 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Player;
using TTT.Shop;
using TTT.Shop.Items;
using Xunit;
namespace TTT.Test.Shop.Items;
public class DeagleTests {
private readonly IServiceProvider provider;
private readonly IEventBus bus;
private readonly TestPlayer testPlayer;
private readonly IOnlinePlayer victim, survivor;
private readonly IShop shop;
private readonly OneShotDeagle item;
public DeagleTests(IServiceProvider provider) {
this.provider = provider;
var games = provider.GetRequiredService<IGameManager>();
var finder = provider.GetRequiredService<IPlayerFinder>();
shop = provider.GetRequiredService<IShop>();
bus = provider.GetRequiredService<IEventBus>();
item = new OneShotDeagle(provider);
testPlayer = (finder.AddPlayer(TestPlayer.Random()) as TestPlayer)!;
victim = finder.AddPlayer(TestPlayer.Random());
survivor = finder.AddPlayer(TestPlayer.Random());
games.CreateGame()?.Start();
}
[Fact]
public void Deagle_Kills_OnDamage() {
bus.RegisterListener(new DeagleDamageListener(provider));
shop.GiveItem(testPlayer, item);
var playerDmgEvent =
new PlayerDamagedEvent(victim, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(playerDmgEvent);
Assert.Equal(0, victim.Health);
Assert.False(victim.IsAlive);
}
[Fact]
public void Deagle_DoesNotKill_AfterFirstKill() {
bus.RegisterListener(new DeagleDamageListener(provider));
shop.GiveItem(testPlayer, item);
var playerDmgEvent =
new PlayerDamagedEvent(victim, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(playerDmgEvent);
var secondDmgEvent =
new PlayerDamagedEvent(survivor, testPlayer, 1, 99) {
Weapon = item.WeaponId
};
bus.Dispatch(secondDmgEvent);
Assert.NotEqual(0, survivor.Health);
}
}

View File

@@ -16,7 +16,15 @@ public class TestPlayer(string id, string name) : IOnlinePlayer {
public int Health { get; set; } = 100;
public int MaxHealth { get; set; } = 100;
public int Armor { get; set; } = 100;
public bool IsAlive { get; set; } = true;
public bool IsAlive {
get => Health > 0;
set {
if (!value)
Health = 0;
else if (Health <= 0) { Health = 1; }
}
}
public static TestPlayer Random() {
return new TestPlayer(new Random().NextInt64().ToString(),