Compare commits

...

29 Commits

Author SHA1 Message Date
MSWS
edbff4e17f feat: Add range limit and refund system to tripwires 2025-11-05 03:49:16 -08:00
MSWS
b4452d7ff3 refactor: Cleanup and reformat 2025-11-05 03:37:56 -08:00
MSWS
fd32744bf6 update: Licenses 2025-11-05 03:34:52 -08:00
MSWS
657306c1c7 feat: Add tripwire CS2 config, boost damage slightly 2025-11-04 18:33:23 -08:00
MSWS
2c800b471b Merge branch 'dev' of github.com:MSWS/TTT into dev 2025-11-03 23:15:22 -08:00
MSWS
2787823f86 fix: Fix how end pos is calculated for tripwires 2025-11-03 23:14:46 -08:00
Isaac
a5f419aad9 Merge branch 'main' into dev 2025-11-03 22:38:26 -08:00
MSWS
430a8c4a7f fix: Nerf tripwire damage, fix damage station giving health 2025-11-03 22:36:17 -08:00
Isaac
a7a44b50f9 feat: Tripwire Item, Pistol Round (#173)
### Features

* Added **Tripwire** item
* Added **[DETECTIVE]** role prefix support for **MAUL**

### Fixes

* Fixed a bug causing **negative damage** to be logged

### Updates

* **Reduced time granted per kill** in speed rounds from **10 → 8
seconds**
* **Rebalanced special round weights**
* **Adjusted killfeed visibility** for traitors
* **Modified hurt stations** to only play the hurt sound **to yourself**
2025-11-03 21:32:25 -08:00
MSWS
3b4bf490bc fix: Negative damage logging, server crashes 2025-11-03 21:26:56 -08:00
MSWS
abe75d0347 fix: Adjust detective role color 2025-11-03 21:13:01 -08:00
MSWS
eb79552ba3 fix: Fix speed round config 2025-11-03 21:11:35 -08:00
MSWS
e2011b8d24 feat: Add pistol rounds (resolves #169) 2025-11-03 21:05:43 -08:00
MSWS
ec41a6f367 feat: Add tripwire item (resolves #165) +semver:minor 2025-11-03 20:51:53 -08:00
MSWS
9b1bed6982 Begin work on tripwire item 2025-11-03 16:53:39 -08:00
MSWS
8584877739 feat: Replicate death events to fellow traitors (resolves #171) +semver:minor 2025-11-03 15:30:44 -08:00
Isaac
7ce5293ad3 Re-apply consistent values between cfgs and cs2 impl. +semver:patch (#172)
- Undid an unnecessary game hook for role assignments
- Make all rounds give 1 karma to all players
- Re-apply body paint price
- Reduce min requirement of players for special rounds from 8 -> 5
2025-11-02 01:26:22 -08:00
Isaac
ae99fab18e Additional Game Balancing, Add +inspect support +semver:minor (#167)
### Features

* Added **Silent Rounds**
* Added **Suppressed Rounds**
* Players can now use `+inspect` (default F) to interact with objects as
an alternative to +use

### Fixes

* Shop now automatically **closes after a purchase**

### Updates

* **Healthshot** cost increased from **25 → 40**
* **Body / Player Compass** cost reduced from **70 → 60**
* **Body Paint** cost reduced from **40 → 30**
* **Camouflage** visibility reduced from **60% → 50%**
* **Speed Round** time capped at **90 seconds**
* **Default weapons removed**
* **Default Detective weapon** changed to **Silenced M4A1**
* **Detective Ratio** reduced during higher populations
2025-11-01 20:03:57 -07:00
Isaac
99ed6bd69b update: Increase speedround weight +semver:patch (#166) 2025-10-31 19:10:12 -07:00
MSWS
79ab6f9705 Force Build + Release 2025-10-31 17:57:49 -07:00
Isaac
80a9cb2af1 fix: Fake traitors getting damaged by hurt stations (#161) 2025-10-30 21:48:23 -07:00
Isaac
c4a73f9a24 Require on actual team to be alive (#160) 2025-10-30 18:17:33 -07:00
Isaac
8fa2377e1e Game Balancing and Fixes (#159) 2025-10-30 18:03:51 -07:00
Isaac
dbe664d18f Revert "Fetch playername from object if available" (#158)
This reverts commit 8cd8e14e18.
2025-10-29 16:30:34 -07:00
Isaac
5717ab612a Fetch playername from object if available +semver:patch (#157) 2025-10-29 15:36:29 -07:00
Isaac
f6b79ef038 QoL tweaks +semver:patch (#156) 2025-10-29 01:45:55 -07:00
Isaac
cc52d19108 fix: Suppress damage stats (+semver:patch) (#155) 2025-10-28 15:32:06 -07:00
Isaac
a1d595ce8a feat: Map integration, Additional Configs, Teleport Item (resolves #152) +semver:minor (#154)
- Use weights for generating special rounds
- Add teleport decoy
- Map hooking for traitor rooms / buttons
- Prevent being able to buy m4a1-s or revolver before round start
- Clean up logs
- Add [BAD] prefix to bad actions
2025-10-28 13:50:57 -07:00
MSWS
23f502a381 Increment version +semver:minor 2025-10-26 23:18:57 -07:00
30 changed files with 1557 additions and 66 deletions

View File

@@ -1,20 +1,17 @@
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| Dapper | 2.1.66 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | 2019 Stack Exchange, Inc. | Sam Saffron,Marc Gravell,Nick Craver | https://github.com/DapperLib/Dapper |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Data.Sqlite | 9.0.9 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://docs.microsoft.com/dotnet/standard/data/sqlite/ |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| MySqlConnector | 2.4.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright 20162024 Bradley Grainger | Bradley Grainger | https://mysqlconnector.net/ |
| SQLite | 3.13.0 | Unknown | | | Public Domain | SQLite Development Team | |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.342 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Data.Sqlite | 9.0.9 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://docs.microsoft.com/dotnet/standard/data/sqlite/ |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |

View File

@@ -1,7 +1,6 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using SpecialRoundAPI.Configs;
using TTT.API;
using TTT.API.Events;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;

View File

@@ -1,5 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record BhopRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.2f;
public override float Weight { get; init; } = 0.25f;
}

View File

@@ -0,0 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record PistolRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.75f;
}

View File

@@ -1,5 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record SilentRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.1f;
public override float Weight { get; init; } = 0.5f;
}

View File

@@ -1,9 +1,9 @@
namespace SpecialRoundAPI.Configs;
public record SpeedRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.6f;
public override float Weight { get; init; } = 1;
public TimeSpan InitialSeconds { get; init; } = TimeSpan.FromSeconds(40);
public TimeSpan SecondsPerKill { get; init; } = TimeSpan.FromSeconds(10);
public TimeSpan SecondsPerKill { get; init; } = TimeSpan.FromSeconds(8);
public TimeSpan MaxTimeEver { get; init; } = TimeSpan.FromSeconds(90);
}

View File

@@ -1,5 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record SuppressedRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.3f;
public override float Weight { get; init; } = 0.75f;
}

View File

@@ -1,5 +1,5 @@
namespace SpecialRoundAPI.Configs;
public record VanillaRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.2f;
public override float Weight { get; init; } = 0.5f;
}

View File

@@ -66,6 +66,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<IStorage<SilentAWPConfig>, CS2SilentAWPConfig>();
collection
.AddModBehavior<IStorage<HealthshotConfig>, CS2HealthshotConfig>();
collection.AddModBehavior<IStorage<TripwireConfig>, CS2TripwireConfig>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();

View File

@@ -48,7 +48,6 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
var purchaseEv = new PlayerPurchaseItemEvent(target, item);
provider.GetRequiredService<IEventBus>().Dispatch(purchaseEv);
if (purchaseEv.IsCanceled) return;
shop.GiveItem(target, item);
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");

View File

@@ -0,0 +1,68 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2TripwireConfig : IStorage<TripwireConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_tripwire_price", "Price of the Tripwire item (Traitor)", 50,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_EXPLOSION_POWER = new(
"css_ttt_shop_tripwire_explosion_power",
"Explosion power of the Tripwire in damage units", 1000,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 10000));
public static readonly FakeConVar<float> CV_FALLOFF_DELAY = new(
"css_ttt_shop_tripwire_falloff_delay",
"Damage falloff, higher means faster falloff", 0.15f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
public static readonly FakeConVar<float> CV_FF_MULTIPLIER = new(
"css_ttt_shop_tripwire_friendlyfire_multiplier",
"Damage multiplier applied to friendly fire from Tripwire", 0.5f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
public static readonly FakeConVar<bool> CV_FF_TRIGGERS = new(
"css_ttt_shop_tripwire_friendlyfire_triggers",
"Whether Tripwires can be triggered by teammates", true);
public static readonly FakeConVar<float> CV_MAX_PLACEMENT_DISTANCE_SQUARED =
new("css_ttt_shop_tripwire_max_placement_distance_squared",
"Maximum distance squared from player to place Tripwire", 400f * 400f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(100f, 100000f));
public static readonly FakeConVar<int> CV_INITIATION_TIME_MS = new(
"css_ttt_shop_tripwire_initiation_time_ms",
"Time in milliseconds to initiate Tripwire placement", 2000,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<TripwireConfig?> Load() {
var cfg = new TripwireConfig {
Price = CV_PRICE.Value,
ExplosionPower = CV_EXPLOSION_POWER.Value,
FalloffDelay = CV_FALLOFF_DELAY.Value,
FriendlyFireMultiplier = CV_FF_MULTIPLIER.Value,
FriendlyFireTriggers = CV_FF_TRIGGERS.Value,
MaxPlacementDistanceSquared = CV_MAX_PLACEMENT_DISTANCE_SQUARED.Value,
TripwireInitiationTime =
TimeSpan.FromMilliseconds(CV_INITIATION_TIME_MS.Value)
};
return Task.FromResult<TripwireConfig?>(cfg);
}
}

View File

@@ -7,8 +7,10 @@ using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.API;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.CS2.GameHandlers;
@@ -21,6 +23,9 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
private readonly IGameManager games =
provider.GetRequiredService<IGameManager>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
@@ -45,7 +50,19 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
if (ev.Attacker != null) ev.FireEventToClient(ev.Attacker);
if (ev.Attacker != null) {
ev.FireEventToClient(ev.Attacker);
var apiPlayer = converter.GetPlayer(ev.Attacker);
var role = roles.GetRoles(apiPlayer);
if (role.Any(r => r is TraitorRole))
foreach (var p in Utilities.GetPlayers()) {
var apiP = converter.GetPlayer(p);
if (apiP.Id == apiPlayer.Id) continue;
var r = roles.GetRoles(converter.GetPlayer(p));
if (role.Intersect(r).Any()) ev.FireEventToClient(p);
}
}
info.DontBroadcast = true;
spoofer.SpoofAlive(player);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));

View File

@@ -1,5 +1,4 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -80,14 +79,14 @@ public class DamageStation(IServiceProvider provider)
foreach (var (player, dist, gamePlayer) in playerDists) {
var healthScale = 1.0 - dist / _Config.MaxRange;
var damageAmount =
(int)Math.Floor(_Config.HealthIncrements * healthScale);
Math.Abs((int)Math.Floor(_Config.HealthIncrements * healthScale));
var dmgEvent = new PlayerDamagedEvent(player,
info.Owner as IOnlinePlayer, damageAmount) { Weapon = $"[{Name}]" };
bus.Dispatch(dmgEvent);
damageAmount = -dmgEvent.DmgDealt;
damageAmount = dmgEvent.DmgDealt;
if (player.Health + damageAmount <= 0) {
killedWithStation[player.Id] = info;
@@ -98,7 +97,7 @@ public class DamageStation(IServiceProvider provider)
}
gamePlayer.EmitSound("Player.DamageFall", SELF(gamePlayer.Slot), 0.2f);
player.Health += damageAmount;
player.Health -= damageAmount;
info.HealthGiven += damageAmount;
}
}

View File

@@ -20,7 +20,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
protected readonly StationConfig _Config = config;
protected readonly IPlayerConverter<CCSPlayerController> Converter =
protected readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly long PROP_SIZE_SQUARED = 700;
@@ -124,7 +124,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
prop.SetModel("models/props/cs_office/microwave.vmdl");
prop.DispatchSpawn();
var gamePlayer = Converter.GetPlayer(player);
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;

View File

@@ -0,0 +1,155 @@
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using TTT.CS2.RayTrace.Enum;
using TTT.CS2.RayTrace.Struct;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Tripwire;
public static class TripwireServiceCollection {
public static void AddTripwireServices(this IServiceCollection services) {
services.AddModBehavior<TripwireItem>();
services.AddModBehavior<TripwireMovementListener>();
}
}
public class TripwireItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule {
private TripwireConfig config
=> Provider.GetService<IStorage<TripwireConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TripwireConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
public readonly List<TripwireInstance> ActiveTripwires = [];
public override string Name => Locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE];
public override string Description
=> Locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_DESC];
public override ShopItemConfig Config => config;
public void Start(BasePlugin? plugin) {
Start();
plugin
?.RegisterListener<
CounterStrikeSharp.API.Core.Listeners.OnServerPrecacheResources>(
onPrecache);
}
private void onPrecache(ResourceManifest manifest) {
manifest.AddResource(
"models/generic/conveyor_control_panel_01/conveyor_button_02.vmdl");
}
public override void OnPurchase(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
if (!placeTripwire(player, out var originTrace, out var endTrace,
out var tripwire))
return;
scheduler.Schedule(config.TripwireInitiationTime,
() => {
Server.NextWorldUpdate(() => {
createTripwireBeam(player, tripwire,
originTrace.Value.EndPos.toVector(),
endTrace.Value.EndPos.toVector());
});
});
});
}
private bool placeTripwire(IOnlinePlayer player,
[NotNullWhen(true)] out CGameTrace? originTrace,
[NotNullWhen(true)] out CGameTrace? endTrace,
[NotNullWhen(true)] out CDynamicProp? tripwire) {
tripwire = null;
originTrace = null;
endTrace = null;
var gamePlayer = converter.GetPlayer(player);
var playerPawn = gamePlayer?.PlayerPawn.Value;
if (gamePlayer == null || playerPawn == null) return false;
originTrace = gamePlayer.GetGameTraceByEyePosition(TraceMask.MaskSolid,
Contents.NoDraw, gamePlayer);
var origin = gamePlayer.GetEyePosition();
if (origin == null || originTrace == null) return false;
if (origin.DistanceSquared(originTrace.Value.EndPos.toVector())
> config.MaxPlacementDistanceSquared) {
Shop.AddBalance(player, config.Price, "Refund");
Messenger.Message(player, Locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_TOOFAR]);
return false;
}
var angles = vectorToAngle(originTrace.Value.Normal.toVector());
endTrace = TraceRay.TraceShape(originTrace.Value.EndPos.toVector(), angles,
TraceMask.MaskSolid, Contents.NoDraw, gamePlayer);
tripwire = Utilities.CreateEntityByName<CDynamicProp>("prop_dynamic");
if (tripwire == null) return false;
tripwire.SetModel(
"models/generic/conveyor_control_panel_01/conveyor_button_02.vmdl");
tripwire.DispatchSpawn();
tripwire.Teleport(originTrace.Value.EndPos.toVector(),
vectorToAngle(originTrace.Value.Normal.toVector()));
tripwire.EmitSound("Weapon_ELITE.Clipout");
return true;
}
private void createTripwireBeam(IOnlinePlayer owner, CDynamicProp prop,
Vector start, Vector end) {
prop.EmitSound("C4.ExplodeTriggerTrip");
var beam = createBeamEnt(start, end);
if (beam == null) return;
var instance = new TripwireInstance(owner, beam, prop, start, end);
ActiveTripwires.Add(instance);
}
private QAngle vectorToAngle(Vector vec) {
var pitch = (float)(Math.Atan2(-vec.Z,
Math.Sqrt(vec.X * vec.X + vec.Y * vec.Y)) * (180.0 / Math.PI));
var yaw = (float)(Math.Atan2(vec.Y, vec.X) * (180.0 / Math.PI));
return new QAngle(pitch, yaw, 0);
}
private CEnvBeam? createBeamEnt(Vector start, Vector end) {
var beam = Utilities.CreateEntityByName<CEnvBeam>("env_beam");
if (beam == null) return null;
beam.RenderMode = RenderMode_t.kRenderTransAlpha;
beam.Width = 0.5f;
beam.Render = Color.FromArgb(128, Color.Red);
beam.EndPos.X = end.X;
beam.EndPos.Y = end.Y;
beam.EndPos.Z = end.Z;
beam.Teleport(start);
return beam;
}
public record TripwireInstance(IOnlinePlayer owner, CEnvBeam Beam,
CDynamicProp TripwireProp, Vector StartPos, Vector EndPos);
}

View File

@@ -0,0 +1,137 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Timers;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs.Traitor;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.CS2.RayTrace.Class;
using TTT.CS2.RayTrace.Enum;
using TTT.Game.Events.Body;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Tripwire;
public class TripwireMovementListener(IServiceProvider provider)
: IPluginModule, IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly TripwireItem? item = provider.GetService<TripwireItem>();
private readonly Dictionary<string, TripwireItem.TripwireInstance>
killedWithTripwire = new();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private TripwireConfig config
=> provider.GetService<IStorage<TripwireConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TripwireConfig();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
if (item == null) return;
plugin?.AddTimer(0.1f, checkTripwires, TimerFlags.REPEAT);
}
private void checkTripwires() {
if (item == null) return;
var toRemove = new List<TripwireItem.TripwireInstance>();
foreach (var wire in item.ActiveTripwires) {
var ray = TraceRay.TraceShape(wire.StartPos, wire.EndPos, Contents.Player,
wire.TripwireProp.Handle);
if (!ray.DidHit() || !ray.HitPlayer(out var player)) continue;
if (!config.FriendlyFireTriggers && player != null) {
var apiPlayer = converter.GetPlayer(player);
var role = roles.GetRoles(apiPlayer);
if (role.Any(r => r is TraitorRole)) continue;
}
toRemove.Add(wire);
wire.TripwireProp.EmitSound("Flashbang.ExplodeDistant");
doExplosion(wire);
}
foreach (var wire in toRemove) {
item.ActiveTripwires.Remove(wire);
wire.Beam.Remove();
wire.TripwireProp.Remove();
}
}
private void doExplosion(TripwireItem.TripwireInstance instance) {
foreach (var player in finder.GetOnline()) {
if (!player.IsAlive) continue;
var gamePlayer = converter.GetPlayer(player);
if (gamePlayer == null || gamePlayer.Pawn.Value == null) continue;
if (gamePlayer.Pawn.Value.AbsOrigin == null) continue;
var distance =
instance.StartPos.Distance(gamePlayer.Pawn.Value.AbsOrigin);
var damage = (int)Math.Round(getDamage(distance));
if (roles.GetRoles(player).Any(r => r is TraitorRole))
damage = (int)(damage * config.FriendlyFireMultiplier);
if (damage < 1) continue;
if (player.Health - damage <= 0) {
killedWithTripwire[player.Id] = instance;
var death = new PlayerDeathEvent(player).WithKiller(instance.owner)
.WithWeapon("[Tripwire]");
bus.Dispatch(death);
} else {
var damaged =
new PlayerDamagedEvent(player, instance.owner, damage) {
Weapon = "[Tripwire]"
};
bus.Dispatch(damaged);
}
player.Health -= damage;
gamePlayer.EmitSound("Player.BurnDamage");
}
}
private float getDamage(float distance) {
return config.ExplosionPower
* MathF.Pow(MathF.E, -distance * config.FalloffDelay);
}
[UsedImplicitly]
[EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
killedWithTripwire.Clear();
}
[UsedImplicitly]
[EventHandler]
public void OnRagdollSpawn(BodyCreateEvent ev) {
if (!killedWithTripwire.TryGetValue(ev.Body.Id, out var info)) return;
if (ev.Body.Killer != null && ev.Body.Killer.Id != ev.Body.OfPlayer.Id)
return;
ev.Body.Killer = info.owner;
}
}

View File

@@ -0,0 +1,14 @@
using TTT.Locale;
namespace TTT.CS2.Items.Tripwire;
public class TripwireMsgs {
public static IMsg SHOP_ITEM_TRIPWIRE
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE));
public static IMsg SHOP_ITEM_TRIPWIRE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE_DESC));
public static IMsg SHOP_ITEM_TRIPWIRE_TOOFAR
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE_TOOFAR));
}

View File

@@ -14,12 +14,11 @@ namespace TTT.CS2.Listeners;
public class WardenTagAssigner(IServiceProvider provider)
: BaseListener(provider) {
private Dictionary<string, (string, char)> oldTags =
new Dictionary<string, (string, char)>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly Dictionary<string, (string, char)> oldTags = new();
[UsedImplicitly]
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
@@ -41,7 +40,7 @@ public class WardenTagAssigner(IServiceProvider provider)
if (ev.Role is DetectiveRole) {
maul.getTagService().SetTag(gamePlayer, "[DETECTIVE]", false);
maul.getTagService()
.SetTagColor(gamePlayer, ChatColors.LightBlue, false);
.SetTagColor(gamePlayer, ChatColors.DarkBlue, false);
} else if (oldTags.TryGetValue(ev.Player.Id, out var oldTag)) {
maul.getTagService().SetTag(gamePlayer, oldTag.Item1, false);
maul.getTagService().SetTagColor(gamePlayer, oldTag.Item2, false);

View File

@@ -52,4 +52,8 @@ SHOP_ITEM_CLUSTER_GRENADE: "Cluster Grenade"
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."
SHOP_ITEM_TELEPORT_DECOY: "Teleport Decoy"
SHOP_ITEM_TELEPORT_DECOY_DESC: "A decoy that teleports you to it upon explosion."
SHOP_ITEM_TELEPORT_DECOY_DESC: "A decoy that teleports you to it upon explosion."
SHOP_ITEM_TRIPWIRE: "Tripwire"
SHOP_ITEM_TRIPWIRE_DESC: "A tripwire that explodes when triggered."
SHOP_ITEM_TRIPWIRE_TOOFAR: "%PREFIX%You are too far away to place the tripwire."

View File

@@ -8,7 +8,7 @@ namespace TTT.Game.Events.Player;
public class PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
int originalHp, int hpLeft) : PlayerEvent(player), ICancelableEvent {
public PlayerDamagedEvent(IOnlinePlayer player, IOnlinePlayer? attacker,
int damageDealt) : this(player, attacker, player.Health - damageDealt,
int damageDealt) : this(player, attacker, player.Health + damageDealt,
player.Health) { }
public PlayerDamagedEvent(IPlayerConverter<CCSPlayerController> converter,

View File

@@ -2,8 +2,8 @@
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml:space="preserve">
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -13,6 +13,7 @@ using TTT.CS2.Items.PoisonSmoke;
using TTT.CS2.Items.SilentAWP;
using TTT.CS2.Items.Station;
using TTT.CS2.Items.TeleportDecoy;
using TTT.CS2.Items.Tripwire;
using TTT.Shop.Commands;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
@@ -61,5 +62,6 @@ public static class ShopServiceCollection {
collection.AddStickerServices();
collection.AddTaserServices();
collection.AddTeleportDecoyServices();
collection.AddTripwireServices();
}
}

View File

@@ -0,0 +1,11 @@
namespace ShopAPI.Configs.Traitor;
public record TripwireConfig : ShopItemConfig {
public override int Price { get; init; } = 60;
public int ExplosionPower { get; init; } = 1000;
public float FalloffDelay { get; init; } = 0.02f;
public float FriendlyFireMultiplier { get; init; } = 0.5f;
public bool FriendlyFireTriggers { get; init; } = true;
public float MaxPlacementDistanceSquared { get; init; } = 400f * 400f;
public TimeSpan TripwireInitiationTime { get; init; } = TimeSpan.FromSeconds(2);
}

View File

@@ -40,7 +40,7 @@ public enum PurchaseResult {
/// <summary>
/// The item cannot be purchased multiple times, and the player already owns it.
/// </summary>
ALREADY_OWNED
ALREADY_OWNED,
}
public static class PurchaseResultExtensions {
@@ -58,7 +58,7 @@ public static class PurchaseResultExtensions {
PurchaseResult.WRONG_ROLE =>
"You do not have the required role to purchase this item",
PurchaseResult.ALREADY_OWNED =>
"You already own this item and cannot purchase it again",
"You have purchased the maximum amount of this item",
_ => "An unexpected error occurred"
};
}

View File

@@ -0,0 +1,66 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using SpecialRound.lang;
using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Locale;
namespace SpecialRound.Rounds;
public class PistolRound(IServiceProvider provider)
: AbstractSpecialRound(provider), IPluginModule {
private readonly IInventoryManager inventory = provider
.GetRequiredService<IInventoryManager>();
private BasePlugin? plugin;
public override string Name => "Pistol";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_PISTOL;
private PistolRoundConfig config
=> Provider.GetService<IStorage<PistolRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new PistolRoundConfig();
public override SpecialRoundConfig Config => config;
public void Start(BasePlugin? newPluing) { plugin = newPluing; }
public override void ApplyRoundEffects() {
VirtualFunctions.CCSPlayer_ItemServices_CanAcquireFunc.Hook(canAcquire,
HookMode.Pre);
foreach (var player in Finder.GetOnline())
inventory.RemoveWeaponInSlot(player, 0);
}
private HookResult canAcquire(DynamicHook hook) {
var player = hook.GetParam<CCSPlayer_ItemServices>(0)
.Pawn.Value.Controller.Value?.As<CCSPlayerController>();
var data = VirtualFunctions.GetCSWeaponDataFromKey.Invoke(-1,
hook.GetParam<CEconItemView>(1).ItemDefinitionIndex.ToString());
if (player == null || !player.IsValid) return HookResult.Continue;
if (Tag.RIFLES.Contains(data.Name)) {
hook.SetReturn(AcquireResult.NotAllowedByMode);
return HookResult.Handled;
}
return HookResult.Continue;
}
public override void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
VirtualFunctions.CCSPlayer_ItemServices_CanAcquireFunc.Unhook(canAcquire,
HookMode.Pre);
}
}

View File

@@ -1,5 +1,4 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.UserMessages;
using Microsoft.Extensions.DependencyInjection;
using SpecialRound.lang;
@@ -15,23 +14,6 @@ namespace SpecialRound.Rounds;
public class SuppressedRound(IServiceProvider provider)
: AbstractSpecialRound(provider), IPluginModule {
public override string Name => "Suppressed";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_SUPPRESSED;
public override SpecialRoundConfig Config => config;
private BasePlugin? plugin;
private SuppressedRoundConfig config
=> Provider.GetService<IStorage<SuppressedRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SuppressedRoundConfig();
public void Start(BasePlugin? newPlugin) { plugin ??= newPlugin; }
public override void ApplyRoundEffects() {
plugin?.HookUserMessage(452, onWeaponSound);
}
private static readonly HashSet<uint> silencedWeapons = new() {
1, // deagle
2, // dual berettas
@@ -45,6 +27,23 @@ public class SuppressedRound(IServiceProvider provider)
64 // r8 revolver
};
private BasePlugin? plugin;
public override string Name => "Suppressed";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_SUPPRESSED;
public override SpecialRoundConfig Config => config;
private SuppressedRoundConfig config
=> Provider.GetService<IStorage<SuppressedRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SuppressedRoundConfig();
public void Start(BasePlugin? newPlugin) { plugin ??= newPlugin; }
public override void ApplyRoundEffects() {
plugin?.HookUserMessage(452, onWeaponSound);
}
private HookResult onWeaponSound(UserMessage msg) {
var defIndex = msg.ReadUInt("item_def_index");

View File

@@ -15,5 +15,6 @@ public static class SpecialRoundCollection {
services.AddModBehavior<VanillaRound>();
services.AddModBehavior<SuppressedRound>();
services.AddModBehavior<SilentRound>();
services.AddModBehavior<PistolRound>();
}
}

View File

@@ -22,6 +22,9 @@ public class RoundMsgs {
public static IMsg SPECIAL_ROUND_SILENT
=> MsgFactory.Create(nameof(SPECIAL_ROUND_SILENT));
public static IMsg SPECIAL_ROUND_PISTOL
=> MsgFactory.Create(nameof(SPECIAL_ROUND_PISTOL));
public static IMsg SPECIAL_ROUND_STARTED(AbstractSpecialRound round) {
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name);
}

View File

@@ -4,4 +4,5 @@ SPECIAL_ROUND_BHOP: " {Yellow}BHOP{grey}: Bunny hopping is enabled! Hold jump to
SPECIAL_ROUND_VANILLA: " {green}VANILLA{grey}: The shop has been disabled!"
SPECIAL_ROUND_SUPPRESSED: " {grey}SUPPRESSED{grey}: All pistols are silent!"
SPECIAL_ROUND_SILENT: " {grey}SILENT{grey}: All players are muted!"
SPECIAL_ROUND_PISTOL: " {blue}PISTOL{grey}: You can only use pistols this round!"
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."

File diff suppressed because it is too large Load Diff