mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-06 06:22:44 -08:00
Compare commits
109 Commits
1.0.1-dev.
...
2.0.3-dev.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba91bfb1cf | ||
|
|
6da8a729ab | ||
|
|
d9f49473eb | ||
|
|
c2ada273a8 | ||
|
|
deb2e1cab2 | ||
|
|
dfe86b0242 | ||
|
|
eff68897a0 | ||
|
|
63afe31e3b | ||
|
|
a7fa2afe15 | ||
|
|
7749deabd3 | ||
|
|
f8b67c5194 | ||
|
|
77281aa8c6 | ||
|
|
20497bbb4d | ||
|
|
133083003d | ||
|
|
ad3603c833 | ||
|
|
6d7149a3f5 | ||
|
|
24cd1295b6 | ||
|
|
125daa515e | ||
|
|
722c29bde7 | ||
|
|
3c0fd74c2a | ||
|
|
60de8b54db | ||
|
|
e7dfbca35d | ||
|
|
3a463b29c6 | ||
|
|
e11a8e20e5 | ||
|
|
b0a7ec60e0 | ||
|
|
0aa28b1076 | ||
|
|
b53920282b | ||
|
|
5167291ab4 | ||
|
|
d76f93c0c7 | ||
|
|
39da6e8702 | ||
|
|
d628df6116 | ||
|
|
da232907f7 | ||
|
|
703144f04b | ||
|
|
3e947959ac | ||
|
|
aed5b4ace4 | ||
|
|
0abbb0c07a | ||
|
|
8131a85bb6 | ||
|
|
8d068b675b | ||
|
|
84d81fc3db | ||
|
|
0531d3afb5 | ||
|
|
eb48bc68f3 | ||
|
|
40b153d938 | ||
|
|
e35551b830 | ||
|
|
bb132b987b | ||
|
|
f92088882e | ||
|
|
3cdf2510bf | ||
|
|
61b8d4eba9 | ||
|
|
1ff4eabd45 | ||
|
|
a0cf6b38f2 | ||
|
|
9145e16b90 | ||
|
|
b3000ecc4e | ||
|
|
f2fde12737 | ||
|
|
e7ed74de0c | ||
|
|
33535abdd7 | ||
|
|
0d8643dfe3 | ||
|
|
50d078f78e | ||
|
|
edbff4e17f | ||
|
|
b4452d7ff3 | ||
|
|
fd32744bf6 | ||
|
|
657306c1c7 | ||
|
|
2c800b471b | ||
|
|
2787823f86 | ||
|
|
29693f99c5 | ||
|
|
a5f419aad9 | ||
|
|
430a8c4a7f | ||
|
|
a7a44b50f9 | ||
|
|
3b4bf490bc | ||
|
|
abe75d0347 | ||
|
|
eb79552ba3 | ||
|
|
e2011b8d24 | ||
|
|
ec41a6f367 | ||
|
|
9b1bed6982 | ||
|
|
8584877739 | ||
|
|
410dd407b3 | ||
|
|
a0bba2c4ba | ||
|
|
8aa508bf6d | ||
|
|
642155b1bc | ||
|
|
bacd288fe7 | ||
|
|
29e28038b8 | ||
|
|
7ce5293ad3 | ||
|
|
b253d8ee12 | ||
|
|
02575b51e2 | ||
|
|
d8d365b497 | ||
|
|
1ac38dc0ad | ||
|
|
62e57ffa97 | ||
|
|
81e6b2e695 | ||
|
|
ae99fab18e | ||
|
|
2ce0457346 | ||
|
|
ed90c54e53 | ||
|
|
06d2d71f76 | ||
|
|
c6ba041a6b | ||
|
|
f283d7407e | ||
|
|
51ff4df545 | ||
|
|
e0ee4bf325 | ||
|
|
4a4c7e0782 | ||
|
|
d4f67ced0c | ||
|
|
33ca0c8385 | ||
|
|
ff2e97a3ce | ||
|
|
99ed6bd69b | ||
|
|
79ab6f9705 | ||
|
|
80a9cb2af1 | ||
|
|
c4a73f9a24 | ||
|
|
8fa2377e1e | ||
|
|
dbe664d18f | ||
|
|
5717ab612a | ||
|
|
f6b79ef038 | ||
|
|
cc52d19108 | ||
|
|
a1d595ce8a | ||
|
|
23f502a381 |
4
.github/workflows/dotnet.yml
vendored
4
.github/workflows/dotnet.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
MAX_CHANGELOG_CHARS: "50000"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
36
LICENSES.MD
36
LICENSES.MD
@@ -1,20 +1,16 @@
|
||||
| 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 2016–2024 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.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 |
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.332"/>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346"/>
|
||||
<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"/>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public record BhopRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.2f;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public record VanillaRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.2f;
|
||||
}
|
||||
2
TTT.sln
2
TTT.sln
@@ -29,7 +29,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stats", "TTT\Stats\Stats.cs
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpecialRound", "TTT\SpecialRound\SpecialRound.csproj", "{5092069A-3CFA-41C8-B685-341040AB435C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpecialRoundAPI", "SpecialRoundAPI\SpecialRoundAPI\SpecialRoundAPI.csproj", "{360FEF16-54DA-42EE-995A-3D31C699287D}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpecialRoundAPI", "TTT\SpecialRoundAPI\SpecialRoundAPI.csproj", "{360FEF16-54DA-42EE-995A-3D31C699287D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
||||
@@ -21,4 +21,55 @@ public interface IPlayerFinder {
|
||||
var matches = GetOnline().Where(p => p.Name.Contains(name)).ToList();
|
||||
return matches.Count == 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
List<IOnlinePlayer> GetMulti(string query, out string name,
|
||||
IOnlinePlayer? executor = null) {
|
||||
var result = query switch {
|
||||
"@all" => GetOnline().ToList(),
|
||||
"@me" => executor != null ? new List<IOnlinePlayer> { executor } : [],
|
||||
"@!me" => executor != null ?
|
||||
GetOnline().Where(p => p.Id != executor.Id).ToList() :
|
||||
GetOnline().ToList(),
|
||||
_ => GetSingle(query) != null ?
|
||||
new List<IOnlinePlayer> { GetSingle(query)! } : []
|
||||
};
|
||||
|
||||
name = "no players found";
|
||||
name = query switch {
|
||||
"@all" => "all players",
|
||||
"@me" => executor != null ? executor.Name : "no one",
|
||||
"@!me" => executor != null ?
|
||||
$"all players except {executor.Name}" :
|
||||
"all players",
|
||||
_ => GetSingle(query) != null ?
|
||||
GetSingle(query)!.Name :
|
||||
"no players found"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
IOnlinePlayer? GetSingle(string query) {
|
||||
if (query.StartsWith("#")) {
|
||||
var id = query[1..];
|
||||
var byId = GetPlayerById(id);
|
||||
if (byId != null) return byId;
|
||||
var byName = GetOnline().FirstOrDefault(p => p.Name == id);
|
||||
return byName;
|
||||
}
|
||||
|
||||
var byNameExact = GetOnline().FirstOrDefault(p => p.Name == query);
|
||||
if (byNameExact != null) return byNameExact;
|
||||
|
||||
var contains = GetOnline().Where(p => p.Name.Contains(query)).ToList();
|
||||
|
||||
if (contains.Count == 1) return contains[0];
|
||||
|
||||
contains = GetOnline()
|
||||
.Where(p
|
||||
=> p.Name.Contains(query, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return contains.Count == 1 ? contains[0] : null;
|
||||
}
|
||||
}
|
||||
5
TTT/CS2/API/Items/ITripwireActivator.cs
Normal file
5
TTT/CS2/API/Items/ITripwireActivator.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace TTT.CS2.API.Items;
|
||||
|
||||
public interface ITripwireActivator {
|
||||
public void ActivateTripwire(TripwireInstance tripwire);
|
||||
}
|
||||
6
TTT/CS2/API/Items/ITripwireTracker.cs
Normal file
6
TTT/CS2/API/Items/ITripwireTracker.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace TTT.CS2.API.Items;
|
||||
|
||||
public interface ITripwireTracker {
|
||||
public List<TripwireInstance> ActiveTripwires { get; }
|
||||
void RemoveTripwire(TripwireInstance instance);
|
||||
}
|
||||
13
TTT/CS2/API/Items/TripwireInstance.cs
Normal file
13
TTT/CS2/API/Items/TripwireInstance.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.API.Items;
|
||||
|
||||
public record TripwireInstance(IOnlinePlayer owner, CEnvBeam Beam,
|
||||
CDynamicProp TripwireProp, Vector StartPos, Vector EndPos) {
|
||||
public override string ToString() {
|
||||
return
|
||||
$"TripwireInstance(Owner={owner}, StartPos={StartPos}, EndPos={EndPos})";
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SpecialRoundAPI\SpecialRoundAPI\SpecialRoundAPI.csproj"/>
|
||||
<ProjectReference Include="..\API\API.csproj"/>
|
||||
<ProjectReference Include="..\Game\Game.csproj"/>
|
||||
<ProjectReference Include="..\Karma\Karma.csproj"/>
|
||||
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
|
||||
<ProjectReference Include="..\SpecialRoundAPI\SpecialRoundAPI.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -66,26 +66,26 @@ 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>();
|
||||
|
||||
// GameHandlers
|
||||
collection.AddModBehavior<BodySpawner>();
|
||||
collection.AddModBehavior<BombPlantSuppressor>();
|
||||
collection.AddModBehavior<BuyMenuHandler>();
|
||||
collection.AddModBehavior<CombatHandler>();
|
||||
collection.AddModBehavior<DamageCanceler>();
|
||||
collection.AddModBehavior<MapChangeCausesEndListener>();
|
||||
collection.AddModBehavior<MapZoneRemover>();
|
||||
collection.AddModBehavior<NameUpdater>();
|
||||
collection.AddModBehavior<PlayerConnectionsHandler>();
|
||||
collection.AddModBehavior<PlayerMuter>();
|
||||
collection.AddModBehavior<PropMover>();
|
||||
collection.AddModBehavior<RoundStart_GameStartHandler>();
|
||||
collection.AddModBehavior<BombPlantSuppressor>();
|
||||
collection.AddModBehavior<MapZoneRemover>();
|
||||
collection.AddModBehavior<BuyMenuHandler>();
|
||||
collection.AddModBehavior<TeamChangeHandler>();
|
||||
collection.AddModBehavior<TraitorChatHandler>();
|
||||
collection.AddModBehavior<PlayerMuter>();
|
||||
collection.AddModBehavior<MapChangeCausesEndListener>();
|
||||
collection.AddModBehavior<NameUpdater>();
|
||||
// collection.AddModBehavior<EntityTargetHandlers>();
|
||||
|
||||
// Damage Cancelers
|
||||
collection.AddModBehavior<OutOfRoundCanceler>();
|
||||
@@ -95,13 +95,14 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<AfkTimerListener>();
|
||||
collection.AddModBehavior<BodyPickupListener>();
|
||||
collection.AddModBehavior<IBodyTracker, BodyTracker>();
|
||||
collection.AddModBehavior<KarmaBanner>();
|
||||
collection.AddModBehavior<KarmaSyncer>();
|
||||
collection.AddModBehavior<LateSpawnListener>();
|
||||
collection.AddModBehavior<MapHookListener>();
|
||||
collection.AddModBehavior<PlayerStatsTracker>();
|
||||
collection.AddModBehavior<RoundTimerListener>();
|
||||
collection.AddModBehavior<ScreenColorApplier>();
|
||||
collection.AddModBehavior<KarmaBanner>();
|
||||
collection.AddModBehavior<KarmaSyncer>();
|
||||
collection.AddModBehavior<MapHookListener>();
|
||||
collection.AddModBehavior<WardenTagAssigner>();
|
||||
|
||||
// Commands
|
||||
collection.AddModBehavior<TestCommand>();
|
||||
|
||||
@@ -41,15 +41,15 @@ public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
|
||||
|
||||
private void onButton(CCSPlayerController? player, int index) {
|
||||
if (player == null) return;
|
||||
if (converter.GetPlayer(player) is not IOnlinePlayer gamePlayer) return;
|
||||
if (converter.GetPlayer(player) is not IOnlinePlayer apiPlayer) return;
|
||||
|
||||
var lastUpdated = itemSorter.GetLastUpdate(gamePlayer);
|
||||
var lastUpdated = itemSorter.GetLastUpdate(apiPlayer);
|
||||
if (lastUpdated == null
|
||||
|| DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20))
|
||||
return;
|
||||
var cmdInfo = new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "buy",
|
||||
(index - 1).ToString());
|
||||
cmdInfo.CallingContext = CommandCallingContext.Chat;
|
||||
var cmdInfo = new CS2CommandInfo(provider, apiPlayer, 0, "css_shop", "buy",
|
||||
(index - 1).ToString()) { CallingContext = CommandCallingContext.Chat };
|
||||
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
|
||||
itemSorter.InvalidateOrder(apiPlayer);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Events;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Command.Test;
|
||||
@@ -33,25 +31,15 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
var target = executor;
|
||||
List<IOnlinePlayer> targets = [executor];
|
||||
|
||||
Server.NextWorldUpdateAsync(() => {
|
||||
if (info.ArgCount == 3) {
|
||||
var result = finder.GetPlayerByName(info.Args[2]);
|
||||
if (result == null) {
|
||||
info.ReplySync($"Player '{info.Args[2]}' not found.");
|
||||
return;
|
||||
}
|
||||
var name = executor.Name;
|
||||
if (info.ArgCount == 3)
|
||||
targets = finder.GetMulti(info.Args[2], out name, executor);
|
||||
foreach (var player in targets) shop.GiveItem(player, item);
|
||||
|
||||
target = result;
|
||||
}
|
||||
|
||||
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}.");
|
||||
info.ReplySync($"Gave item '{item.Name}' to {name}.");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public string Id => "setrole";
|
||||
@@ -24,7 +27,10 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
|
||||
|
||||
IRole roleToAssign = new TraitorRole(provider);
|
||||
// IOnlinePlayer targetPlayer = executor;
|
||||
List<IOnlinePlayer> targets = [executor];
|
||||
var targetName = executor.Name;
|
||||
IRole roleToAssign = new TraitorRole(provider);
|
||||
if (info.ArgCount == 2)
|
||||
switch (info.Args[1].ToLowerInvariant()) {
|
||||
case "d" or "det" or "detective" or "ct":
|
||||
@@ -33,18 +39,29 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
case "i" or "inn" or "innocent":
|
||||
roleToAssign = new InnocentRole(provider);
|
||||
break;
|
||||
default:
|
||||
targets = finder.GetMulti(info.Args[1], out targetName, executor);
|
||||
break;
|
||||
}
|
||||
|
||||
if (info.ArgCount == 3)
|
||||
targets = finder.GetMulti(info.Args[2], out targetName, executor);
|
||||
|
||||
Server.NextWorldUpdate(() => {
|
||||
var ev = new PlayerRoleAssignEvent(executor, roleToAssign);
|
||||
bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) {
|
||||
info.ReplySync("Role assignment was canceled.");
|
||||
return;
|
||||
foreach (var player in targets) {
|
||||
var ev = new PlayerRoleAssignEvent(player, roleToAssign);
|
||||
bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) {
|
||||
info.ReplySync("Role assignment was canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
assigner.Write(player, [ev.Role]);
|
||||
ev.Role.OnAssign(player);
|
||||
}
|
||||
|
||||
assigner.Write(executor, [ev.Role]);
|
||||
ev.Role.OnAssign(executor);
|
||||
info.ReplySync(
|
||||
"Assigned " + roleToAssign.Name + " to " + targetName + ".");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public class SpecCommand(IServiceProvider provider) : ICommand {
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public string Id => "spec";
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
var target = executor;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRoundAPI;
|
||||
using TTT.API;
|
||||
using TTT.API.Command;
|
||||
@@ -7,8 +8,8 @@ using TTT.API.Player;
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly ISpecialRoundStarter tracker =
|
||||
provider.GetRequiredService<ISpecialRoundStarter>();
|
||||
private readonly ISpecialRoundStarter? tracker =
|
||||
provider.GetService<ISpecialRoundStarter>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
@@ -17,25 +18,32 @@ public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
if (tracker == null) {
|
||||
info.ReplySync("Special round tracker is not available.");
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
if (info.ArgCount == 1) {
|
||||
tracker.TryStartSpecialRound(null);
|
||||
Server.NextWorldUpdate(() => tracker.TryStartSpecialRound());
|
||||
info.ReplySync("Started a random special round.");
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
var rounds = provider.GetServices<ITerrorModule>()
|
||||
.OfType<AbstractSpecialRound>()
|
||||
.ToDictionary(r => r.GetType().Name.ToLower(), r => r);
|
||||
.ToDictionary(r => r.Name.ToLower().Replace(" ", ""), r => r);
|
||||
|
||||
var roundName = info.Args[1].ToLower();
|
||||
var roundName = string.Join("", info.Args.Skip(1)).ToLower();
|
||||
if (!rounds.TryGetValue(roundName, out var round)) {
|
||||
info.ReplySync($"No special round found with name '{roundName}'.");
|
||||
foreach (var name in rounds.Keys) info.ReplySync($"- {name}");
|
||||
return Task.FromResult(CommandResult.INVALID_ARGS);
|
||||
}
|
||||
|
||||
tracker.TryStartSpecialRound(round);
|
||||
info.ReplySync($"Started special round '{roundName}'.");
|
||||
Server.NextWorldUpdate(() => {
|
||||
tracker.TryStartSpecialRound([round]);
|
||||
info.ReplySync($"Started special round '{roundName}'.");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
}
|
||||
@@ -48,36 +48,34 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ARMOR = new(
|
||||
"css_ttt_rolearmor_traitor",
|
||||
"Amount of armor to give to traitors at start of round", 100,
|
||||
"Amount of armor to give to traitors at start of round", 0,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_DETECTIVE_ARMOR = new(
|
||||
"css_ttt_rolearmor_detective",
|
||||
"Amount of armor to give to detectives at start of round", 100,
|
||||
"Amount of armor to give to detectives at start of round", 0,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNOCENT_ARMOR = new(
|
||||
"css_ttt_rolearmor_innocent",
|
||||
"Amount of armor to give to innocents at start of round", 100,
|
||||
"Amount of armor to give to innocents at start of round", 0,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_TRAITOR_WEAPONS = new(
|
||||
"css_ttt_roleweapons_traitor",
|
||||
"Weapons available to traitors at start of round",
|
||||
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE,
|
||||
new ItemValidator(allowMultiple: true));
|
||||
"Weapons available to traitors at start of round", "",
|
||||
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
|
||||
|
||||
public static readonly FakeConVar<string> CV_DETECTIVE_WEAPONS = new(
|
||||
"css_ttt_roleweapons_detective",
|
||||
"Weapons available to detectives at start of round",
|
||||
"weapon_knife,weapon_taser,weapon_m4a1,weapon_revolver",
|
||||
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
|
||||
"weapon_taser,weapon_m4a1_silencer,weapon_revolver", ConVarFlags.FCVAR_NONE,
|
||||
new ItemValidator(allowMultiple: true));
|
||||
|
||||
public static readonly FakeConVar<string> CV_INNOCENT_WEAPONS = new(
|
||||
"css_ttt_roleweapons_innocent",
|
||||
"Weapons available to innocents at start of round",
|
||||
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE,
|
||||
new ItemValidator(allowMultiple: true));
|
||||
"Weapons available to innocents at start of round", "",
|
||||
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new(
|
||||
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1,
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
// Karma deltas
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_TRAITOR = new(
|
||||
"css_ttt_karma_inno_on_traitor",
|
||||
"Karma gained when Innocent kills a Traitor", 4, ConVarFlags.FCVAR_NONE,
|
||||
"Karma gained when Innocent kills a Traitor", 2, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
|
||||
@@ -55,32 +55,32 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO_VICTIM = new(
|
||||
"css_ttt_karma_inno_on_inno_victim",
|
||||
"Karma gained or lost when Innocent kills another Innocent who was a victim",
|
||||
-1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
-2, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
|
||||
"css_ttt_karma_inno_on_inno",
|
||||
"Karma lost when Innocent kills another Innocent", -5,
|
||||
"Karma lost when Innocent kills another Innocent", -8,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_TRAITOR = new(
|
||||
"css_ttt_karma_traitor_on_traitor",
|
||||
"Karma lost when Traitor kills another Traitor", -6, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
"Karma lost when Traitor kills another Traitor", -12,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_DETECTIVE = new(
|
||||
"css_ttt_karma_inno_on_detective",
|
||||
"Karma lost when Innocent kills a Detective", -8, ConVarFlags.FCVAR_NONE,
|
||||
"Karma lost when Innocent kills a Detective", -15, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
|
||||
"css_ttt_karma_per_round",
|
||||
"Amount of karma a player will gain at the end of each round", 2,
|
||||
"Amount of karma a player will gain at the end of each round", 1,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new(
|
||||
"css_ttt_karma_per_round_win",
|
||||
"Amount of karma a player will gain at the end of each round if their team won",
|
||||
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
|
||||
1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
|
||||
|
||||
public class CS2BodyPaintConfig : IStorage<BodyPaintConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_PRICE = new(
|
||||
"css_ttt_shop_bodypaint_price", "Price of the Body Paint item", 40,
|
||||
"css_ttt_shop_bodypaint_price", "Price of the Body Paint item", 30,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_MAX_USES = new(
|
||||
|
||||
@@ -16,7 +16,7 @@ public class CS2CamoConfig : IStorage<CamoConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<float> CV_CAMO_VISIBILITY = new(
|
||||
"css_ttt_shop_camo_visibility",
|
||||
"Player visibility multiplier while camouflaged (0 = invisible, 1 = fully visible)",
|
||||
0.6f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
|
||||
0.5f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
|
||||
|
||||
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_PRICE = new(
|
||||
"css_ttt_shop_taser_price", "Price of the Taser item", 120,
|
||||
"css_ttt_shop_taser_price", "Price of the Taser item", 110,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_WEAPON = new(
|
||||
|
||||
116
TTT/CS2/Configs/ShopItems/CS2TripwireConfig.cs
Normal file
116
TTT/CS2/Configs/ShopItems/CS2TripwireConfig.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System.Drawing;
|
||||
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)", 45,
|
||||
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 of tripwire explosion, higher = quicker falloff", 0.015f,
|
||||
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_DISTANCE_SQUARED = new(
|
||||
"css_ttt_shop_tripwire_max_distance_squared",
|
||||
"Maximum placement distance squared for Tripwire", 50000f,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1000000f));
|
||||
|
||||
public static readonly FakeConVar<float> CV_INITIATION_TIME = new(
|
||||
"css_ttt_shop_tripwire_initiation_time",
|
||||
"Seconds before Tripwire becomes active", 2f, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<float>(0f, 10f));
|
||||
|
||||
public static readonly FakeConVar<float> CV_SIZE_SQUARED = new(
|
||||
"css_ttt_shop_tripwire_size_squared",
|
||||
"Size of tripwire for the purposes of bullet/defuse-detection", 10f,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(1f, 100000f));
|
||||
|
||||
public static readonly FakeConVar<int> CV_COLOR_R = new(
|
||||
"css_ttt_shop_tripwire_color_r", "Tripwire color red channel (0–255)", 255,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 255));
|
||||
|
||||
public static readonly FakeConVar<int> CV_COLOR_G = new(
|
||||
"css_ttt_shop_tripwire_color_g", "Tripwire color green channel (0–255)", 0,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 255));
|
||||
|
||||
public static readonly FakeConVar<int> CV_COLOR_B = new(
|
||||
"css_ttt_shop_tripwire_color_b", "Tripwire color blue channel (0–255)", 0,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 255));
|
||||
|
||||
public static readonly FakeConVar<int> CV_COLOR_A = new(
|
||||
"css_ttt_shop_tripwire_color_a", "Tripwire color alpha (0–255)", 32,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 255));
|
||||
|
||||
public static readonly FakeConVar<float> CV_THICKNESS = new(
|
||||
"css_ttt_shop_tripwire_thickness", "Visual thickness of the Tripwire beam",
|
||||
0.5f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0.01f, 5f));
|
||||
|
||||
public static readonly FakeConVar<float> CV_DEFUSE_TIME = new(
|
||||
"css_ttt_shop_tripwire_defuse_time",
|
||||
"Time required to fully defuse the Tripwire (in seconds)", 6f,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 30f));
|
||||
|
||||
public static readonly FakeConVar<float> CV_DEFUSE_RATE = new(
|
||||
"css_ttt_shop_tripwire_defuse_rate",
|
||||
"Rate at which Tripwire defuses are processed (in seconds)", 0.5f,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0.01f, 5f));
|
||||
|
||||
public static readonly FakeConVar<int> CV_DEFUSE_REWARD = new(
|
||||
"css_ttt_shop_tripwire_defuse_reward",
|
||||
"Amount of money rewarded to a player for successfully defusing a tripwire",
|
||||
20, 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_DISTANCE_SQUARED.Value,
|
||||
TripwireInitiationTime = TimeSpan.FromSeconds(CV_INITIATION_TIME.Value),
|
||||
TripwireSizeSquared = CV_SIZE_SQUARED.Value,
|
||||
TripwireColor =
|
||||
Color.FromArgb(CV_COLOR_A.Value, CV_COLOR_R.Value, CV_COLOR_G.Value,
|
||||
CV_COLOR_B.Value),
|
||||
TripwireThickness = CV_THICKNESS.Value,
|
||||
DefuseTime = TimeSpan.FromSeconds(CV_DEFUSE_TIME.Value),
|
||||
DefuseRate = TimeSpan.FromSeconds(CV_DEFUSE_RATE.Value),
|
||||
DefuseReward = CV_DEFUSE_REWARD.Value
|
||||
};
|
||||
|
||||
return Task.FromResult<TripwireConfig?>(cfg);
|
||||
}
|
||||
}
|
||||
@@ -97,4 +97,11 @@ public static class VectorExtensions {
|
||||
public static Vector toVector(this Vector3 vec) {
|
||||
return new Vector(vec.X, vec.Y, vec.Z);
|
||||
}
|
||||
|
||||
public static QAngle toAngle(this 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using TTT.API.Game;
|
||||
@@ -20,7 +19,8 @@ public class MapChangeCausesEndListener(IServiceProvider provider)
|
||||
}
|
||||
|
||||
private void onMapChange(string mapName) {
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS or State.COUNTDOWN })
|
||||
return;
|
||||
games.ActiveGame?.EndGame(new EndReason("Map Change"));
|
||||
Server.PrintToConsole("Detected map change, ending active game.");
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,9 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pressed.HasFlag(PlayerButtons.Use)) return;
|
||||
if (!pressed.HasFlag(PlayerButtons.Use)
|
||||
&& !pressed.HasFlag(PlayerButtons.Inspect))
|
||||
return;
|
||||
|
||||
onStartUse(player);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,20 @@ using System.Reactive.Concurrency;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using SpecialRoundAPI;
|
||||
using TTT.API;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.RayTrace.Class;
|
||||
using TTT.Game.Events.Body;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Events.Player;
|
||||
@@ -49,11 +53,16 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
foreach (var timer in poisonTimers) timer.Dispose();
|
||||
}
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
base.Start();
|
||||
plugin?.HookUserMessage(452, onWeaponSound);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnFire(EventWeaponFire ev, GameEventInfo _) {
|
||||
if (ev.Userid == null) return HookResult.Continue;
|
||||
if (!Tag.GUNS.Contains(ev.Weapon)) return HookResult.Continue;
|
||||
if (!Tag.PISTOLS.Contains(ev.Weapon)) return HookResult.Continue;
|
||||
if (converter.GetPlayer(ev.Userid) is not IOnlinePlayer player)
|
||||
return HookResult.Continue;
|
||||
var remainingShots = usePoisonShot(player);
|
||||
@@ -156,6 +165,47 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
return 0;
|
||||
}
|
||||
|
||||
private HookResult onWeaponSound(UserMessage msg) {
|
||||
var defIndex = msg.ReadUInt("item_def_index");
|
||||
|
||||
if (!WeaponSoundIndex.PISTOLS.Contains(defIndex))
|
||||
return HookResult.Continue;
|
||||
var splits = msg.DebugString.Split("\n");
|
||||
if (splits.Length < 5) return HookResult.Continue;
|
||||
var angleLines = msg.DebugString.Split("\n")[1..4]
|
||||
.Select(s => s.Trim())
|
||||
.ToList();
|
||||
if (!angleLines[0].Contains('x') || !angleLines[1].Contains('y')
|
||||
|| !angleLines[2].Contains('z'))
|
||||
return HookResult.Continue;
|
||||
var x = float.Parse(angleLines[0].Split(' ')[1]);
|
||||
var y = float.Parse(angleLines[1].Split(' ')[1]);
|
||||
var z = float.Parse(angleLines[2].Split(' ')[1]);
|
||||
var vec = new Vector(x, y, z);
|
||||
var player = findPlayerByCoord(vec);
|
||||
|
||||
if (player == null) return HookResult.Continue;
|
||||
if (converter.GetPlayer(player) is not IOnlinePlayer apiPlayer)
|
||||
return HookResult.Continue;
|
||||
|
||||
if (poisonShots.TryGetValue(apiPlayer, out var shots) && shots > 0) {
|
||||
msg.Recipients.Clear();
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
private CCSPlayerController? findPlayerByCoord(Vector vec) {
|
||||
foreach (var pl in Utilities.GetPlayers()) {
|
||||
var origin = pl.GetEyePosition();
|
||||
if (origin == null) continue;
|
||||
var dist = vec.DistanceSquared(origin);
|
||||
if (dist < 1) return pl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
@@ -29,9 +30,6 @@ public class DamageStation(IServiceProvider provider)
|
||||
.GetResult() ?? new DamageStationConfig()), IListener {
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
@@ -45,24 +43,28 @@ public class DamageStation(IServiceProvider provider)
|
||||
override protected void onInterval() {
|
||||
var players = finder.GetOnline();
|
||||
var toRemove = new List<CPhysicsPropMultiplayer>();
|
||||
foreach (var (prop, info) in props) {
|
||||
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
|
||||
> Math.Abs(_Config.TotalHealthGiven)) {
|
||||
toRemove.Add(prop);
|
||||
continue;
|
||||
}
|
||||
var playerMapping = players
|
||||
.Select(p => (ApiPlayer: p, GamePlayer: Converter.GetPlayer(p)))
|
||||
.Where(m
|
||||
=> m.GamePlayer != null
|
||||
&& !Roles.GetRoles(m.ApiPlayer).Any(r => r is TraitorRole))
|
||||
.ToList();
|
||||
|
||||
if (!prop.IsValid || prop.AbsOrigin == null) {
|
||||
// accumulate contributions per player: ApiPlayer -> list of (stationInfo, damage, gamePlayer)
|
||||
var playerDamageMap =
|
||||
new Dictionary<IOnlinePlayer, List<(StationInfo info, int damage,
|
||||
CCSPlayerController gamePlayer)>>();
|
||||
|
||||
foreach (var (prop, info) in Props) {
|
||||
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
|
||||
> Math.Abs(_Config.TotalHealthGiven) || !prop.IsValid
|
||||
|| prop.AbsOrigin == null) {
|
||||
toRemove.Add(prop);
|
||||
continue;
|
||||
}
|
||||
|
||||
var propPos = prop.AbsOrigin;
|
||||
|
||||
var playerMapping = players.Select(p
|
||||
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
|
||||
.Where(m => m.GamePlayer != null);
|
||||
|
||||
var playerDists = playerMapping
|
||||
.Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin,
|
||||
t.GamePlayer))
|
||||
@@ -73,34 +75,75 @@ public class DamageStation(IServiceProvider provider)
|
||||
.ToList();
|
||||
|
||||
foreach (var (player, dist, gamePlayer) in playerDists) {
|
||||
gamePlayer.EmitSound("Player.DamageFall", null, 0.2f);
|
||||
if (Roles.GetRoles(player).Any(r => r is TraitorRole)) continue;
|
||||
|
||||
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}]" };
|
||||
if (damageAmount <= 0) continue;
|
||||
|
||||
bus.Dispatch(dmgEvent);
|
||||
|
||||
damageAmount = -dmgEvent.DmgDealt;
|
||||
|
||||
if (player.Health + damageAmount <= 0) {
|
||||
killedWithStation[player.Id] = info;
|
||||
var playerDeath = new PlayerDeathEvent(player)
|
||||
.WithKiller(info.Owner as IOnlinePlayer)
|
||||
.WithWeapon($"[{Name}]");
|
||||
bus.Dispatch(playerDeath);
|
||||
if (!playerDamageMap.TryGetValue(player, out var list)) {
|
||||
list = [];
|
||||
playerDamageMap[player] = list;
|
||||
}
|
||||
|
||||
player.Health += damageAmount;
|
||||
info.HealthGiven += damageAmount;
|
||||
list.Add((info, damageAmount, gamePlayer));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in toRemove) props.Remove(prop);
|
||||
// Apply accumulated damage per player once
|
||||
applyDamage(playerDamageMap);
|
||||
|
||||
// remove invalid/expired props
|
||||
foreach (var prop in toRemove) Props.Remove(prop);
|
||||
}
|
||||
|
||||
private void applyDamage(
|
||||
Dictionary<IOnlinePlayer, List<(StationInfo info, int damage,
|
||||
CCSPlayerController gamePlayer)>> playerDamageMap) {
|
||||
foreach (var kv in playerDamageMap) {
|
||||
var player = kv.Key;
|
||||
var contribs = kv.Value;
|
||||
|
||||
var totalDamage = contribs.Sum(c => c.damage);
|
||||
|
||||
if (totalDamage <= 0) continue;
|
||||
|
||||
// choose the station that contributed the most damage to attribute the kill to
|
||||
var dominantInfo = contribs.OrderByDescending(c => c.damage).First().info;
|
||||
var gamePlayer = contribs.First().gamePlayer;
|
||||
|
||||
// dispatch single PlayerDamagedEvent with total damage
|
||||
var dmgEvent = new PlayerDamagedEvent(player,
|
||||
dominantInfo.Owner as IOnlinePlayer, totalDamage) {
|
||||
Weapon = $"[{Name}]"
|
||||
};
|
||||
|
||||
bus.Dispatch(dmgEvent);
|
||||
|
||||
totalDamage = dmgEvent.DmgDealt;
|
||||
|
||||
// if this will kill the player, attribute death to the dominant station
|
||||
if (player.Health - totalDamage <= 0) {
|
||||
killedWithStation[player.Id] = dominantInfo;
|
||||
var playerDeath = new PlayerDeathEvent(player)
|
||||
.WithKiller(dominantInfo.Owner as IOnlinePlayer)
|
||||
.WithWeapon($"[{Name}]");
|
||||
bus.Dispatch(playerDeath);
|
||||
}
|
||||
|
||||
gamePlayer.EmitSound("Player.DamageFall", SELF(gamePlayer.Slot), 0.2f);
|
||||
|
||||
// apply damage to player's health
|
||||
player.Health -= totalDamage;
|
||||
|
||||
// update each station's HealthGiven by its own contribution
|
||||
foreach (var (info, damage, _) in contribs) info.HealthGiven += damage;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static RecipientFilter SELF(int slot) {
|
||||
return new RecipientFilter(slot);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
|
||||
@@ -30,7 +30,29 @@ public class HealthStation(IServiceProvider provider)
|
||||
override protected void onInterval() {
|
||||
var players = Utilities.GetPlayers();
|
||||
var toRemove = new List<CPhysicsPropMultiplayer>();
|
||||
foreach (var (prop, info) in props) {
|
||||
|
||||
// build per-player potential heal contributions and gather props to remove
|
||||
var perPlayerContrib = BuildPerPlayerContributions(players, toRemove);
|
||||
|
||||
// apply the accumulated heals in a single pass per player
|
||||
applyAccumulatedHeals(perPlayerContrib);
|
||||
|
||||
// remove invalid/expired props
|
||||
foreach (var prop in toRemove) Props.Remove(prop);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scan all props and build a map: Player -> list of (StationInfo, potentialHeal).
|
||||
/// Also fills toRemove for invalid/expired props.
|
||||
/// </summary>
|
||||
private
|
||||
Dictionary<CCSPlayerController, List<(StationInfo info, int potential)>>
|
||||
BuildPerPlayerContributions(IReadOnlyList<CCSPlayerController> players,
|
||||
List<CPhysicsPropMultiplayer> toRemove) {
|
||||
var result =
|
||||
new Dictionary<CCSPlayerController, List<(StationInfo, int)>>();
|
||||
|
||||
foreach (var (prop, info) in Props) {
|
||||
if (_Config.TotalHealthGiven != 0
|
||||
&& Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
|
||||
toRemove.Add(prop);
|
||||
@@ -52,19 +74,130 @@ public class HealthStation(IServiceProvider provider)
|
||||
.ToList();
|
||||
|
||||
foreach (var (player, dist) in playerDists) {
|
||||
var maxHp = player.Pawn.Value?.MaxHealth ?? 100;
|
||||
var healthScale = 1.0 - dist / _Config.MaxRange;
|
||||
var maxHealAmo =
|
||||
(int)Math.Ceiling(_Config.HealthIncrements * healthScale);
|
||||
var newHealth = Math.Min(player.GetHealth() + maxHealAmo, maxHp);
|
||||
var healthGiven = newHealth - player.GetHealth();
|
||||
player.SetHealth(newHealth);
|
||||
info.HealthGiven += healthGiven;
|
||||
var potentialHeal = ComputePotentialHeal(dist);
|
||||
if (potentialHeal <= 0) continue;
|
||||
|
||||
if (healthGiven > 0) player.EmitSound("HealthShot.Pickup", null, 0.1f);
|
||||
if (!result.TryGetValue(player, out var list)) {
|
||||
list = [];
|
||||
result[player] = list;
|
||||
}
|
||||
|
||||
list.Add((info, potentialHeal));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in toRemove) props.Remove(prop);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute potential heal from a station given the distance.
|
||||
/// Mirrors your original logic: ceil(HealthIncrements * healthScale).
|
||||
/// </summary>
|
||||
private int ComputePotentialHeal(double dist) {
|
||||
var healthScale = 1.0 - dist / _Config.MaxRange;
|
||||
return (int)Math.Ceiling(_Config.HealthIncrements * healthScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply heals to each player once per tick.
|
||||
/// Distributes the actual heal proportionally across contributing stations,
|
||||
/// updates station.Info.HealthGiven and emits a single pickup sound if needed.
|
||||
/// </summary>
|
||||
private void applyAccumulatedHeals(
|
||||
Dictionary<CCSPlayerController, List<(StationInfo info, int potential)>>
|
||||
perPlayerContrib) {
|
||||
foreach (var kv in perPlayerContrib) {
|
||||
var player = kv.Key;
|
||||
var contribs = kv.Value;
|
||||
|
||||
var maxHp = player.Pawn.Value?.MaxHealth ?? 100;
|
||||
var currentHp = player.GetHealth();
|
||||
if (currentHp >= maxHp) continue;
|
||||
|
||||
var totalPotential = contribs.Sum(c => c.potential);
|
||||
if (totalPotential <= 0) continue;
|
||||
|
||||
var totalHealAvailable = Math.Min(totalPotential, maxHp - currentHp);
|
||||
if (totalHealAvailable <= 0) continue;
|
||||
|
||||
var potentials = contribs.Select(c => c.potential).ToList();
|
||||
var allocations =
|
||||
allocateProportionalInteger(totalHealAvailable, potentials);
|
||||
|
||||
// safety clamp: never allocate more than a station's potential
|
||||
for (var i = 0; i < allocations.Count; i++)
|
||||
if (allocations[i] > potentials[i])
|
||||
allocations[i] = potentials[i];
|
||||
|
||||
// if clamping reduced the total, try to redistribute remaining amount
|
||||
var allocatedSum = allocations.Sum();
|
||||
if (allocatedSum < totalHealAvailable) {
|
||||
var remaining = totalHealAvailable - allocatedSum;
|
||||
for (var i = 0; i < allocations.Count && remaining > 0; i++) {
|
||||
var headroom = potentials[i] - allocations[i];
|
||||
if (headroom <= 0) continue;
|
||||
var give = Math.Min(headroom, remaining);
|
||||
allocations[i] += give;
|
||||
remaining -= give;
|
||||
}
|
||||
|
||||
allocatedSum = allocations.Sum();
|
||||
}
|
||||
|
||||
// apply heal to player
|
||||
var actualAllocated = Math.Min(allocatedSum, maxHp - currentHp);
|
||||
if (actualAllocated <= 0) continue;
|
||||
|
||||
var newHealth = Math.Min(currentHp + actualAllocated, maxHp);
|
||||
player.SetHealth(newHealth);
|
||||
|
||||
// update station HealthGiven
|
||||
for (var i = 0; i < allocations.Count; i++) {
|
||||
var info = contribs[i].info;
|
||||
var allocated = allocations[i];
|
||||
if (allocated > 0) info.HealthGiven += allocated;
|
||||
}
|
||||
|
||||
// emit a single pickup sound if any heal applied
|
||||
player.EmitSound("HealthShot.Pickup", null, 0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proportionally distribute an integer total across integer potentials.
|
||||
/// Uses floor of shares and assigns leftover to largest fractional remainders.
|
||||
/// Returns a list of allocations with same length as potentials.
|
||||
/// </summary>
|
||||
private List<int>
|
||||
allocateProportionalInteger(int total, List<int> potentials) {
|
||||
var allocations = new List<int>(new int[potentials.Count]);
|
||||
var remainders = new List<(int idx, double rem)>();
|
||||
var sumBase = 0;
|
||||
var totalPotential = potentials.Sum();
|
||||
|
||||
if (totalPotential <= 0) return allocations;
|
||||
|
||||
for (var i = 0; i < potentials.Count; i++) {
|
||||
var potential = potentials[i];
|
||||
var share = (double)potential / totalPotential * total;
|
||||
var baseAlloc = (int)Math.Floor(share);
|
||||
var rem = share - baseAlloc;
|
||||
allocations[i] = baseAlloc;
|
||||
sumBase += baseAlloc;
|
||||
remainders.Add((i, rem));
|
||||
}
|
||||
|
||||
var leftover = total - sumBase;
|
||||
if (leftover <= 0) return allocations;
|
||||
|
||||
remainders = remainders.OrderByDescending(r => r.rem).ToList();
|
||||
var idx = 0;
|
||||
while (leftover > 0 && idx < remainders.Count) {
|
||||
allocations[remainders[idx].idx] += 1;
|
||||
leftover--;
|
||||
idx++;
|
||||
}
|
||||
|
||||
return allocations;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
|
||||
|
||||
private readonly long PROP_SIZE_SQUARED = 700;
|
||||
|
||||
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
|
||||
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> Props =
|
||||
new();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
@@ -60,7 +60,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
|
||||
public HookResult OnBulletImpact(EventBulletImpact ev, GameEventInfo info) {
|
||||
var hitVec = new Vector(ev.X, ev.Y, ev.Z);
|
||||
|
||||
var nearest = props
|
||||
var nearest = Props
|
||||
.Select(kv => (kv.Key, kv.Value,
|
||||
Distance: kv.Key.AbsOrigin!.DistanceSquared(hitVec)))
|
||||
.Where(t => t.Key is { IsValid: true, AbsOrigin: not null })
|
||||
@@ -76,7 +76,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
|
||||
|
||||
if (nearest.Value.Health <= 0) {
|
||||
nearest.Key.AcceptInput("Kill");
|
||||
props.Remove(nearest.Key);
|
||||
Props.Remove(nearest.Key);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public abstract class StationItem<T>(IServiceProvider provider,
|
||||
|
||||
if (prop == null) return;
|
||||
|
||||
props[prop] = new StationInfo(prop, _Config.StationHealth, player);
|
||||
Props[prop] = new StationInfo(prop, _Config.StationHealth, player);
|
||||
|
||||
prop.SetModel("models/props/cs_office/microwave.vmdl");
|
||||
prop.DispatchSpawn();
|
||||
|
||||
47
TTT/CS2/Items/Tripwire/TripwireDamageListener.cs
Normal file
47
TTT/CS2/Items/Tripwire/TripwireDamageListener.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.API.Items;
|
||||
using TTT.CS2.Extensions;
|
||||
|
||||
namespace TTT.CS2.Items.Tripwire;
|
||||
|
||||
public class TripwireDamageListener(IServiceProvider provider) : IPluginModule {
|
||||
private readonly ITripwireActivator? tripwireActivator =
|
||||
provider.GetRequiredService<ITripwireActivator>();
|
||||
|
||||
private readonly ITripwireTracker? tripwires =
|
||||
provider.GetService<ITripwireTracker>();
|
||||
|
||||
private TripwireConfig config
|
||||
=> provider.GetService<IStorage<TripwireConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TripwireConfig();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnBulletImpact(EventBulletImpact ev, GameEventInfo info) {
|
||||
if (tripwires == null) return HookResult.Continue;
|
||||
var hitVec = new Vector(ev.X, ev.Y, ev.Z);
|
||||
|
||||
var nearest = tripwires.ActiveTripwires
|
||||
.OrderBy(wire => wire.TripwireProp.AbsOrigin.DistanceSquared(hitVec))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (nearest == null) return HookResult.Continue;
|
||||
var distSquared = nearest.TripwireProp.AbsOrigin.DistanceSquared(hitVec);
|
||||
if (distSquared > config.TripwireSizeSquared) return HookResult.Continue;
|
||||
|
||||
tripwireActivator?.ActivateTripwire(nearest);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
}
|
||||
129
TTT/CS2/Items/Tripwire/TripwireDefuserListener.cs
Normal file
129
TTT/CS2/Items/Tripwire/TripwireDefuserListener.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.API.Items;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.RayTrace.Class;
|
||||
using TTT.CS2.RayTrace.Enum;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.Tripwire;
|
||||
|
||||
public class TripwireDefuserListener(IServiceProvider provider)
|
||||
: IPluginModule {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IMsgLocalizer locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
private readonly ITripwireTracker? tripwireTracker =
|
||||
provider.GetService<ITripwireTracker>();
|
||||
|
||||
private TripwireConfig config
|
||||
=> provider.GetService<IStorage<TripwireConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TripwireConfig();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
if (tripwireTracker == null) return;
|
||||
plugin
|
||||
?.RegisterListener<
|
||||
CounterStrikeSharp.API.Core.Listeners.OnPlayerButtonsChanged>(
|
||||
onButtonsChanged);
|
||||
}
|
||||
|
||||
private void onButtonsChanged(CCSPlayerController player,
|
||||
PlayerButtons pressed, PlayerButtons released) {
|
||||
if (tripwireTracker == null) return;
|
||||
if ((pressed & PlayerButtons.Use) != PlayerButtons.Use) return;
|
||||
|
||||
var instance = getTargetTripwire(player);
|
||||
if (instance == null) return;
|
||||
|
||||
startDefuseTimer(player, instance);
|
||||
}
|
||||
|
||||
private TripwireInstance? getTargetTripwire(CCSPlayerController player) {
|
||||
if (tripwireTracker == null) return null;
|
||||
var raytrace =
|
||||
player.GetGameTraceByEyePosition(TraceMask.MaskSolid, Contents.NoDraw,
|
||||
player);
|
||||
|
||||
if (raytrace == null || tripwireTracker.ActiveTripwires.Count == 0)
|
||||
return null;
|
||||
var raytracePos = raytrace.Value.EndPos.toVector();
|
||||
|
||||
var closest =
|
||||
tripwireTracker?.ActiveTripwires.MinBy(i
|
||||
=> i.StartPos.DistanceSquared(raytracePos));
|
||||
|
||||
if (closest == null || closest.StartPos.DistanceSquared(raytracePos)
|
||||
> config.TripwireSizeSquared)
|
||||
return null;
|
||||
|
||||
if (player.Pawn.Value?.AbsOrigin == null
|
||||
|| closest.StartPos.DistanceSquared(player.Pawn.Value.AbsOrigin)
|
||||
> config.MaxPlacementDistanceSquared)
|
||||
return null;
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
private void startDefuseTimer(CCSPlayerController player,
|
||||
TripwireInstance instance) {
|
||||
tickDefuse(player, instance, DateTime.Now);
|
||||
}
|
||||
|
||||
private void tickDefuse(CCSPlayerController player, TripwireInstance instance,
|
||||
DateTime startTime) {
|
||||
if (!player.IsValid) return;
|
||||
var apiPlayer = converter.GetPlayer(player);
|
||||
|
||||
if ((player.Buttons & PlayerButtons.Use) != PlayerButtons.Use) {
|
||||
messenger.Message(apiPlayer,
|
||||
locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_DEFUSING_CANCELED]);
|
||||
return;
|
||||
}
|
||||
|
||||
var progress = (DateTime.Now - startTime) / config.DefuseTime;
|
||||
var timeLeft = config.DefuseTime - config.DefuseTime * progress;
|
||||
|
||||
if (progress >= 1) {
|
||||
instance.TripwireProp.EmitSound("c4.disarmfinish", null, 0.2f, 1.5f);
|
||||
tripwireTracker?.RemoveTripwire(instance);
|
||||
if (apiPlayer is IOnlinePlayer online)
|
||||
provider.GetService<IShop>()
|
||||
?.AddBalance(online, config.DefuseReward, "Tripwire Defusal");
|
||||
return;
|
||||
}
|
||||
|
||||
var target = getTargetTripwire(player);
|
||||
if (target != instance) {
|
||||
messenger.Message(apiPlayer,
|
||||
locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_DEFUSING_CANCELED]);
|
||||
return;
|
||||
}
|
||||
|
||||
player.PrintToCenter(
|
||||
locale[TripwireMsgs.SHOP_ITEM_TRIPWIRE_DEFUSING(progress, timeLeft)]);
|
||||
var pitch = 1.5f - (float)progress;
|
||||
player.EmitSound("c4.keypressquiet", pitch: pitch);
|
||||
var ticksDelay = (int)Math.Round(64 * config.DefuseRate.TotalSeconds);
|
||||
Server.RunOnTick(Server.TickCount + ticksDelay,
|
||||
() => tickDefuse(player, instance, startTime));
|
||||
}
|
||||
}
|
||||
165
TTT/CS2/Items/Tripwire/TripwireItem.cs
Normal file
165
TTT/CS2/Items/Tripwire/TripwireItem.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive.Concurrency;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.API.Items;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.RayTrace.Class;
|
||||
using TTT.CS2.RayTrace.Enum;
|
||||
using TTT.CS2.RayTrace.Struct;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.Tripwire;
|
||||
|
||||
public static class TripwireServiceCollection {
|
||||
public static void AddTripwireServices(this IServiceCollection services) {
|
||||
services.AddModBehavior<ITripwireTracker, TripwireItem>();
|
||||
services.AddModBehavior<ITripwireActivator, TripwireMovementListener>();
|
||||
services.AddModBehavior<TripwireDamageListener>();
|
||||
services.AddModBehavior<TripwireDefuserListener>();
|
||||
}
|
||||
}
|
||||
|
||||
public class TripwireItem(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider), IPluginModule, ITripwireTracker {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private TripwireConfig config
|
||||
=> Provider.GetService<IStorage<TripwireConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TripwireConfig();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public List<TripwireInstance> ActiveTripwires { get; } = [];
|
||||
|
||||
public void RemoveTripwire(TripwireInstance instance) {
|
||||
instance.Beam.Remove();
|
||||
instance.TripwireProp.Remove();
|
||||
ActiveTripwires.Remove(instance);
|
||||
}
|
||||
|
||||
private void onPrecache(ResourceManifest manifest) {
|
||||
manifest.AddResource(
|
||||
"models/generic/conveyor_control_panel_01/conveyor_button_02.vmdl");
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnGameEvent(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
ActiveTripwires.Clear();
|
||||
}
|
||||
|
||||
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 = originTrace.Value.Normal.toVector().toAngle();
|
||||
|
||||
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(),
|
||||
originTrace.Value.Normal.toVector().toAngle());
|
||||
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 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 = config.TripwireThickness;
|
||||
beam.Render = config.TripwireColor;
|
||||
beam.EndPos.X = end.X;
|
||||
beam.EndPos.Y = end.Y;
|
||||
beam.EndPos.Z = end.Z;
|
||||
beam.Teleport(start);
|
||||
return beam;
|
||||
}
|
||||
}
|
||||
137
TTT/CS2/Items/Tripwire/TripwireMovementListener.cs
Normal file
137
TTT/CS2/Items/Tripwire/TripwireMovementListener.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
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.Storage;
|
||||
using TTT.CS2.API.Items;
|
||||
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.Listeners;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.Tripwire;
|
||||
|
||||
public class TripwireMovementListener(IServiceProvider provider)
|
||||
: BaseListener(provider), IPluginModule, ITripwireActivator {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly Dictionary<string, TripwireInstance> killedWithTripwire =
|
||||
new();
|
||||
|
||||
private readonly ITripwireTracker? tripwireTracker =
|
||||
provider.GetService<ITripwireTracker>();
|
||||
|
||||
private TripwireConfig config
|
||||
=> Provider.GetService<IStorage<TripwireConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TripwireConfig();
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
if (tripwireTracker == null) return;
|
||||
plugin?.AddTimer(0.2f, checkTripwires, TimerFlags.REPEAT);
|
||||
}
|
||||
|
||||
public void ActivateTripwire(TripwireInstance instance) {
|
||||
tripwireTracker?.RemoveTripwire(instance);
|
||||
instance.TripwireProp.EmitSound("Flashbang.ExplodeDistant");
|
||||
|
||||
foreach (var player in Finder.GetOnline()) {
|
||||
if (!dealTripwireDamage(instance, player, out var gamePlayer)) continue;
|
||||
gamePlayer.EmitSound("Player.BurnDamage");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkTripwires() {
|
||||
if (tripwireTracker == null) return;
|
||||
foreach (var wire in new List<TripwireInstance>(tripwireTracker
|
||||
.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;
|
||||
}
|
||||
|
||||
ActivateTripwire(wire);
|
||||
}
|
||||
}
|
||||
|
||||
private float getDamage(float distance) {
|
||||
return config.ExplosionPower
|
||||
* MathF.Pow(MathF.E, -distance * config.FalloffDelay);
|
||||
}
|
||||
|
||||
private int getDamage(CCSPlayerController gamePlayer, IOnlinePlayer player,
|
||||
Vector tripwire) {
|
||||
var origin = gamePlayer.Pawn.Value?.AbsOrigin;
|
||||
if (origin == null) return 0;
|
||||
var distance = tripwire.Distance(origin);
|
||||
var damage = getDamage(distance);
|
||||
if (Roles.GetRoles(player).Any(r => r is TraitorRole))
|
||||
damage *= config.FriendlyFireMultiplier;
|
||||
|
||||
return (int)Math.Floor(damage);
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
private bool dealTripwireDamage(TripwireInstance instance,
|
||||
IOnlinePlayer player,
|
||||
[NotNullWhen(true)] out CCSPlayerController? gamePlayer) {
|
||||
gamePlayer = null;
|
||||
if (!player.IsAlive) return false;
|
||||
|
||||
gamePlayer = converter.GetPlayer(player);
|
||||
if (gamePlayer == null) return false;
|
||||
var damage = getDamage(gamePlayer, player, instance.StartPos);
|
||||
|
||||
if (damage < 1) return false;
|
||||
|
||||
Event ev;
|
||||
if (player.Health - damage <= 0) {
|
||||
killedWithTripwire[player.Id] = instance;
|
||||
ev = new PlayerDeathEvent(player).WithKiller(instance.owner)
|
||||
.WithWeapon("[Tripwire]");
|
||||
} else {
|
||||
ev = new PlayerDamagedEvent(player, instance.owner, damage) {
|
||||
Weapon = "[Tripwire]"
|
||||
};
|
||||
}
|
||||
|
||||
Bus.Dispatch(ev);
|
||||
|
||||
player.Health -= damage;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
23
TTT/CS2/Items/Tripwire/TripwireMsgs.cs
Normal file
23
TTT/CS2/Items/Tripwire/TripwireMsgs.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
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));
|
||||
|
||||
public static IMsg SHOP_ITEM_TRIPWIRE_DEFUSING_CANCELED
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE_DEFUSING_CANCELED));
|
||||
|
||||
public static IMsg
|
||||
SHOP_ITEM_TRIPWIRE_DEFUSING(double progress, TimeSpan time) {
|
||||
return MsgFactory.Create(nameof(SHOP_ITEM_TRIPWIRE_DEFUSING),
|
||||
progress.ToString("P0"), time.TotalSeconds.ToString("F1"));
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ public class BodyPickupListener(IServiceProvider provider)
|
||||
if (ev.Player is not IOnlinePlayer online)
|
||||
throw new InvalidOperationException("Player is not an online player.");
|
||||
|
||||
if (ev.Player.Id == body.OfPlayer.Id) return;
|
||||
|
||||
var identifyEvent = new BodyIdentifyEvent(body, online);
|
||||
|
||||
Bus.Dispatch(identifyEvent);
|
||||
|
||||
@@ -19,24 +19,20 @@ public class MapHookListener(IServiceProvider provider)
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
|
||||
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
|
||||
Server.NextWorldUpdate(() => {
|
||||
var player = converter.GetPlayer(ev.Player);
|
||||
if (player == null) return;
|
||||
var player = converter.GetPlayer(ev.Player);
|
||||
if (player == null) return;
|
||||
|
||||
switch (ev.Role) {
|
||||
case TraitorRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null, "TRAITOR:1");
|
||||
break;
|
||||
case DetectiveRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null,
|
||||
"DETECTIVE:1");
|
||||
break;
|
||||
case InnocentRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null,
|
||||
"INNOCENT:1");
|
||||
break;
|
||||
}
|
||||
});
|
||||
switch (ev.Role) {
|
||||
case TraitorRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null, "TRAITOR:1");
|
||||
break;
|
||||
case DetectiveRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null, "DETECTIVE:1");
|
||||
break;
|
||||
case InnocentRole:
|
||||
player.Pawn.Value?.AcceptInput("AddContext", null, null, "INNOCENT:1");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
|
||||
66
TTT/CS2/Listeners/WardenTagAssigner.cs
Normal file
66
TTT/CS2/Listeners/WardenTagAssigner.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.CS2.ThirdParties.eGO;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Listeners;
|
||||
|
||||
public class WardenTagAssigner(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly Dictionary<string, (string, char)> oldTags = new();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
|
||||
var maul = EgoApi.MAUL.Get();
|
||||
if (maul == null) return;
|
||||
if (ev.Role is not DetectiveRole) return;
|
||||
var gamePlayer = converter.GetPlayer(ev.Player);
|
||||
if (gamePlayer == null) return;
|
||||
|
||||
Task.Run(async () => {
|
||||
var oldTag = await maul.getTagService().GetTag(gamePlayer.SteamID);
|
||||
var oldTagColor =
|
||||
await maul.getTagService().GetTagColor(gamePlayer.SteamID);
|
||||
if (oldTag != "[DETECTIVE]")
|
||||
oldTags[ev.Player.Id] = (oldTag, oldTagColor);
|
||||
|
||||
await Server.NextWorldUpdateAsync(() => {
|
||||
maul.getTagService().SetTag(gamePlayer, "[DETECTIVE]", false);
|
||||
maul.getTagService()
|
||||
.SetTagColor(gamePlayer, ChatColors.DarkBlue, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnGameEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
var maul = EgoApi.MAUL.Get();
|
||||
if (maul == null) return;
|
||||
|
||||
foreach (var (playerId, (oldTag, oldTagColor)) in oldTags) {
|
||||
var apiPlayer = Finder.GetPlayerById(playerId);
|
||||
if (apiPlayer == null) continue;
|
||||
var csPlayer = converter.GetPlayer(apiPlayer);
|
||||
if (csPlayer == null || !csPlayer.IsValid) continue;
|
||||
maul.getTagService().SetTag(csPlayer, oldTag, false);
|
||||
maul.getTagService().SetTagColor(csPlayer, oldTagColor, false);
|
||||
}
|
||||
|
||||
oldTags.Clear();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using TTT.API.Player;
|
||||
using TTT.CS2.Extensions;
|
||||
|
||||
namespace TTT.CS2.Player;
|
||||
|
||||
@@ -87,18 +88,17 @@ public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
|
||||
}
|
||||
|
||||
public int Armor {
|
||||
get => Player?.PawnArmor ?? 0;
|
||||
get => Player != null && Player.IsValid ? Player.GetArmor().Item1 : 0;
|
||||
|
||||
set {
|
||||
if (Player == null) return;
|
||||
Player.PawnArmor = value;
|
||||
Utilities.SetStateChanged(Player, "CCSPlayerController", "m_iPawnArmor");
|
||||
if (Player == null || !Player.IsValid) return;
|
||||
Player.SetArmor(value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAlive {
|
||||
get
|
||||
=> Player != null && Player is {
|
||||
=> Player != null && Player.IsValid && Player is {
|
||||
Team : CsTeam.CounterTerrorist or CsTeam.Terrorist,
|
||||
Pawn.Value.Health: > 0
|
||||
};
|
||||
|
||||
@@ -8,48 +8,54 @@ AFK_MOVED: "%PREFIX%You were moved to spectators for being AFK."
|
||||
DEAD_MUTE_REMINDER: "%PREFIX%You are dead and cannot be heard."
|
||||
|
||||
SHOP_ITEM_DNA: "DNA Scanner"
|
||||
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them."
|
||||
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them"
|
||||
SHOP_ITEM_DNA_SCANNED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, their killer was {red}{2}{grey}."
|
||||
SHOP_ITEM_DNA_SCANNED_OTHER: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, {2}."
|
||||
SHOP_ITEM_DNA_EXPIRED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, but the DNA has expired."
|
||||
|
||||
SHOP_ITEM_STATION_HEALTH: "Health Station"
|
||||
SHOP_ITEM_STATION_HEALTH_DESC: "A health station that heals players around it."
|
||||
SHOP_ITEM_STATION_HEALTH_DESC: "The health station will heall all players around it"
|
||||
SHOP_ITEM_STATION_HURT: "Hurt Station"
|
||||
SHOP_ITEM_STATION_HURT_DESC: "A station that hurts non-Traitors around it."
|
||||
SHOP_ITEM_STATION_HURT_DESC: "The hurt station will damage all non-Traitors around it"
|
||||
|
||||
SHOP_ITEM_CAMO: "Camouflage"
|
||||
SHOP_ITEM_CAMO_DESC: "Disguise yourself and make yourself harder to see."
|
||||
SHOP_ITEM_CAMO_DESC: "You are now harder to see"
|
||||
|
||||
SHOP_ITEM_BODY_PAINT: "Body Paint"
|
||||
SHOP_ITEM_BODY_PAINT_DESC: "Paint bodies to make them appear identified."
|
||||
SHOP_ITEM_BODY_PAINT_DESC: "Interacting with bodies will now paint them, making them appear identified"
|
||||
SHOP_ITEM_BODY_PAINT_OUT: "%PREFIX% You ran out of body paint."
|
||||
|
||||
SHOP_ITEM_POISON_SHOTS: "Poison Shots"
|
||||
SHOP_ITEM_POISON_SHOTS_DESC: "Your bullets are coated in a mildly poisonous substance."
|
||||
SHOP_ITEM_POISON_SHOTS_DESC: "The next 5 shots from your {red}pistols{grey} are coated with poison"
|
||||
SHOP_ITEM_POISON_HIT: "%PREFIX% You hit {green}{0}{grey} with a {lightpurple}poison shot{grey}."
|
||||
SHOP_ITEM_POISON_OUT: "%PREFIX% You are out of poison shots."
|
||||
|
||||
SHOP_ITEM_POISON_SMOKE: "Poison Smoke"
|
||||
SHOP_ITEM_POISON_SMOKE_DESC: "Throw a grenade that releases poisonous gas."
|
||||
SHOP_ITEM_POISON_SMOKE_DESC: "The smoke grenade will damage all non-Traitors inside it over time"
|
||||
|
||||
SHOP_ITEM_ARMOR: "Armor with Helmet"
|
||||
SHOP_ITEM_ARMOR_DESC: "Wear armor that reduces incoming damage."
|
||||
SHOP_ITEM_ARMOR_DESC: ""
|
||||
|
||||
SHOP_ITEM_ONE_HIT_KNIFE: "One-Hit Knife"
|
||||
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your next knife hit will be a guaranteed kill."
|
||||
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your {red}next knife{grey} attack will instantly kill your target"
|
||||
|
||||
SHOP_ITEM_COMPASS_PLAYER: "Player Compass"
|
||||
SHOP_ITEM_COMPASS_PLAYER_DESC: "Reveals the direction that the nearest non-Traitor is in."
|
||||
SHOP_ITEM_COMPASS_PLAYER_DESC: ""
|
||||
|
||||
SHOP_ITEM_COMPASS_BODY: "Body Compass"
|
||||
SHOP_ITEM_COMPASS_BODY_DESC: "Reveals the direction that the nearest unidentified body is in."
|
||||
SHOP_ITEM_COMPASS_BODY_DESC: ""
|
||||
|
||||
SHOP_ITEM_SILENT_AWP: "Silent AWP"
|
||||
SHOP_ITEM_SILENT_AWP_DESC: "Receive a silenced AWP with limited ammo."
|
||||
SHOP_ITEM_SILENT_AWP_DESC: ""
|
||||
|
||||
SHOP_ITEM_CLUSTER_GRENADE: "Cluster Grenade"
|
||||
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."
|
||||
SHOP_ITEM_CLUSTER_GRENADE_DESC: ""
|
||||
|
||||
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: "The decoy will teleport you to its location {default}once it explodes{grey}"
|
||||
|
||||
SHOP_ITEM_TRIPWIRE: "Tripwire"
|
||||
SHOP_ITEM_TRIPWIRE_DESC: "The tripwire will activate once anyone crosses it"
|
||||
SHOP_ITEM_TRIPWIRE_TOOFAR: "%PREFIX%You are too far away to place the tripwire."
|
||||
SHOP_ITEM_TRIPWIRE_DEFUSING: "Defusing... ({0}, {1} second%s% left)."
|
||||
SHOP_ITEM_TRIPWIRE_DEFUSING_CANCELED: "%PREFIX%You stopped defusing the tripwire."
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.342"/>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.346"/>
|
||||
<PackageReference Include="System.Reactive" Version="6.0.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,20 +9,21 @@ public record TTTConfig {
|
||||
public Func<int, int> TraitorCount { get; init; } =
|
||||
p => (int)Math.Ceiling((p - 1) / 5f);
|
||||
|
||||
public Func<int, int> DetectiveCount { get; init; } =
|
||||
p => (int)Math.Floor(p / 8f);
|
||||
public Func<int, int> DetectiveCount { get; init; } = p
|
||||
=> (int)Math.Ceiling(Math.Floor(p / 8f) / 1.5f);
|
||||
|
||||
public Func<int, int> InnocentCount { get; init; } = p
|
||||
=> p - (int)Math.Ceiling((p - 1) / 5f) - (int)Math.Floor(p / 8f);
|
||||
=> p - (int)Math.Ceiling((p - 1) / 5f)
|
||||
- (int)Math.Ceiling(Math.Floor(p / 8f) / 1.5f);
|
||||
}
|
||||
|
||||
public record RoleConfig {
|
||||
public int TraitorHealth { get; init; } = 100;
|
||||
public int DetectiveHealth { get; init; } = 100;
|
||||
public int InnocentHealth { get; init; } = 100;
|
||||
public int TraitorArmor { get; init; } = 100;
|
||||
public int DetectiveArmor { get; init; } = 100;
|
||||
public int InnocentArmor { get; init; } = 100;
|
||||
public int TraitorArmor { get; init; }
|
||||
public int DetectiveArmor { get; init; }
|
||||
public int InnocentArmor { get; init; }
|
||||
|
||||
public string[]? TraitorWeapons { get; init; } = ["knife", "pistol"];
|
||||
|
||||
|
||||
@@ -51,9 +51,14 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
}
|
||||
|
||||
var result = shop.TryPurchase(executor, item);
|
||||
return Task.FromResult(result == PurchaseResult.SUCCESS ?
|
||||
CommandResult.SUCCESS :
|
||||
CommandResult.ERROR);
|
||||
if (result == PurchaseResult.SUCCESS) {
|
||||
info.ReplySync(locale[ShopMsgs.SHOP_PURCHASED(item)]);
|
||||
if (!string.IsNullOrWhiteSpace(item.Description))
|
||||
info.ReplySync(locale[ShopMsgs.SHOP_PURCHASED_DETAIL(item)]);
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
private IShopItem? searchItem(IOnlinePlayer? player, string query) {
|
||||
|
||||
@@ -74,6 +74,12 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void InvalidateOrder(IOnlinePlayer? player) {
|
||||
if (player == null) return;
|
||||
cache.Remove(player.Id);
|
||||
lastUpdate.Remove(player.Id);
|
||||
}
|
||||
|
||||
private List<IShopItem> calculateSortedItems(IOnlinePlayer? player) {
|
||||
var items = new List<IShopItem>(shop.Items).Where(item
|
||||
=> player == null
|
||||
|
||||
@@ -72,4 +72,8 @@ public class ShopCommand(IServiceProvider provider) : ICommand, IItemSorter {
|
||||
public DateTime? GetLastUpdate(IOnlinePlayer? player) {
|
||||
return listCmd.GetLastUpdate(player);
|
||||
}
|
||||
|
||||
public void InvalidateOrder(IOnlinePlayer? player) {
|
||||
listCmd.InvalidateOrder(player);
|
||||
}
|
||||
}
|
||||
@@ -54,5 +54,6 @@ public class DeagleDamageListener(IServiceProvider provider)
|
||||
}
|
||||
|
||||
ev.HpLeft = -100;
|
||||
Messenger.Message(victim, Locale[DeagleMsgs.SHOP_ITEM_DEAGLE_VICTIM]);
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,7 @@ public class DeagleMsgs {
|
||||
|
||||
public static IMsg SHOP_ITEM_DEAGLE_HIT_FF
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_HIT_FF));
|
||||
|
||||
public static IMsg SHOP_ITEM_DEAGLE_VICTIM
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_VICTIM));
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class C4ShopItem(IServiceProvider provider)
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
c4sBought++;
|
||||
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
|
||||
}
|
||||
|
||||
@@ -46,13 +47,10 @@ public class C4ShopItem(IServiceProvider provider)
|
||||
if (c4sBought > config.MaxC4PerRound)
|
||||
return PurchaseResult.ITEM_NOT_PURCHASABLE;
|
||||
|
||||
if (config.MaxC4AtOnce > 0) {
|
||||
var count = 0;
|
||||
if (finder.GetOnline()
|
||||
.Where(p => Shop.HasItem<C4ShopItem>(p))
|
||||
.Any(_ => count++ >= config.MaxC4AtOnce))
|
||||
if (config.MaxC4AtOnce > 0)
|
||||
if (finder.GetOnline().Count(p => Shop.HasItem<C4ShopItem>(p))
|
||||
> config.MaxC4AtOnce)
|
||||
return PurchaseResult.ITEM_NOT_PURCHASABLE;
|
||||
}
|
||||
|
||||
return base.CanPurchase(player);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop;
|
||||
|
||||
@@ -23,6 +24,9 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IMsgLocalizer localizer =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly Dictionary<string, List<Vector>> playerPositions = new();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
@@ -67,7 +71,8 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
var position = count == 1 ? 1f : (float)(count - i - 1) / (count - 1);
|
||||
var rewardAmount = scaleRewardAmount(position, config.MinRewardAmount,
|
||||
config.MaxRewardAmount);
|
||||
shop.AddBalance(player, rewardAmount, "Exploration");
|
||||
shop.AddBalance(player, rewardAmount,
|
||||
localizer[ShopMsgs.SHOP_EXPLORATION]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,11 +58,8 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
|
||||
bus.Dispatch(purchaseEvent);
|
||||
if (purchaseEvent.IsCanceled) return PurchaseResult.PURCHASE_CANCELED;
|
||||
|
||||
AddBalance(player, -cost, item.Name);
|
||||
AddBalance(player, -cost, item.Name, printReason);
|
||||
GiveItem(player, item);
|
||||
|
||||
if (printReason)
|
||||
messenger?.Message(player, localizer[ShopMsgs.SHOP_PURCHASED(item)]);
|
||||
return PurchaseResult.SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
<RootNamespace>TTT.Shop</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
|
||||
<HintPath>..\..\..\..\..\..\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\8.0.0\lib\net8.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\API\API.csproj"/>
|
||||
<ProjectReference Include="..\Game\Game.csproj"/>
|
||||
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ public static class ShopMsgs {
|
||||
|
||||
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
|
||||
|
||||
public static IMsg SHOP_EXPLORATION
|
||||
=> MsgFactory.Create(nameof(SHOP_EXPLORATION));
|
||||
|
||||
public static IMsg SHOP_CANNOT_PURCHASE
|
||||
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));
|
||||
|
||||
@@ -19,6 +22,10 @@ public static class ShopMsgs {
|
||||
return MsgFactory.Create(nameof(SHOP_PURCHASED), item.Name);
|
||||
}
|
||||
|
||||
public static IMsg SHOP_PURCHASED_DETAIL(IShopItem item) {
|
||||
return MsgFactory.Create(nameof(SHOP_PURCHASED_DETAIL), item.Description);
|
||||
}
|
||||
|
||||
public static IMsg SHOP_ITEM_NOT_FOUND(string query) {
|
||||
return MsgFactory.Create(nameof(SHOP_ITEM_NOT_FOUND), query);
|
||||
}
|
||||
|
||||
@@ -2,36 +2,40 @@ SHOP_PREFIX: "{green}SHOP {grey}| "
|
||||
SHOP_INACTIVE: "%SHOP_PREFIX%The shop is currently closed."
|
||||
SHOP_ITEM_NOT_FOUND: "%SHOP_PREFIX%Could not find an item named \"{default}{0}{grey}\"."
|
||||
|
||||
SHOP_EXPLORATION: "Exploration"
|
||||
|
||||
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
|
||||
SHOP_ITEM_DEAGLE_DESC: "A one-hit kill revolver with a single bullet. Aim carefully!"
|
||||
SHOP_ITEM_DEAGLE_DESC: "If you hit an enemy, they will die instantly. Hitting a teammate will kill you instead"
|
||||
SHOP_ITEM_DEAGLE_HIT_FF: "%PREFIX%You hit a teammate!"
|
||||
SHOP_ITEM_DEAGLE_VICTIM: "%PREFIX%You were hit by a {yellow}One-Hit Revolver{grey}."
|
||||
|
||||
SHOP_ITEM_STICKERS: "Stickers"
|
||||
SHOP_ITEM_STICKERS_DESC: "Reveal the roles of all players you taser to others."
|
||||
SHOP_ITEM_STICKERS_HIT: "%SHOP_PREFIX%You got stickered, your role is now visible to everyone."
|
||||
SHOP_ITEM_STICKERS_DESC: "When you tase a player, their role will be shown to everyone"
|
||||
SHOP_ITEM_STICKERS_HIT: "%SHOP_PREFIX%You got {green}stickered{grey}, your role is now visible to everyone."
|
||||
|
||||
SHOP_ITEM_C4: "C4 Explosive"
|
||||
SHOP_ITEM_C4_DESC: "A powerful explosive that blows up after a delay."
|
||||
SHOP_ITEM_C4_DESC: "The bomb will deal damage to everyone including you and fellow {red}Traitors{grey}"
|
||||
|
||||
SHOP_ITEM_M4A1: "M4A1 Rifle and USP-S"
|
||||
SHOP_ITEM_M4A1_DESC: "A fully automatic rifle with a silencer accompanied by a silenced pistol."
|
||||
SHOP_ITEM_M4A1_DESC: ""
|
||||
|
||||
SHOP_ITEM_GLOVES: "Gloves"
|
||||
SHOP_ITEM_GLOVES_DESC: "Lets you kill without DNA being left behind, or move bodies without identifying the body."
|
||||
SHOP_ITEM_GLOVES_DESC: "You can now kill without leaving DNA behind, or move bodies without IDing them"
|
||||
SHOP_ITEM_GLOVES_USED_BODY: "%SHOP_PREFIX%You used your gloves to move a body without leaving DNA. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_USED_KILL: "%SHOP_PREFIX%You used your gloves to kill without leaving DNA evidence. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_WORN_OUT: "%SHOP_PREFIX%Your gloves worn out."
|
||||
SHOP_ITEM_GLOVES_WORN_OUT: "%SHOP_PREFIX%Your gloves wore out."
|
||||
|
||||
SHOP_ITEM_TASER: "Taser"
|
||||
SHOP_ITEM_TASER_DESC: "A taser that allows you to identify the roles of players you hit."
|
||||
SHOP_ITEM_TASER_DESC: "Tasing a player will tell you their role"
|
||||
|
||||
SHOP_ITEM_HEALTHSHOT: "Healthshot"
|
||||
SHOP_ITEM_HEALTHSHOT_DESC: "A healthshot that heals you gradually for 50 health."
|
||||
SHOP_ITEM_HEALTHSHOT_DESC: ""
|
||||
|
||||
SHOP_INSUFFICIENT_BALANCE: "%SHOP_PREFIX%You cannot afford {white}{0}{grey}, it costs {yellow}{1}{grey} %CREDITS_NAME%%s%, and you have {yellow}{2}{grey}."
|
||||
SHOP_CANNOT_PURCHASE: "%SHOP_PREFIX%You cannot purchase this item."
|
||||
SHOP_CANNOT_PURCHASE_WITH_REASON: "%SHOP_PREFIX%You cannot purchase this item: {red}{0}{grey}."
|
||||
SHOP_PURCHASED: "%SHOP_PREFIX%You purchased {white}{0}{grey}."
|
||||
SHOP_PURCHASED_DETAIL: "%SHOP_PREFIX%{white}{0}{grey}."
|
||||
SHOP_LIST_FOOTER: "%SHOP_PREFIX%You are %an% {0}{grey}, you have {yellow}{1}{grey} %CREDITS_NAME%%s%."
|
||||
|
||||
CREDITS_NAME: "point"
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Drawing;
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record BodyPaintConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 40;
|
||||
public override int Price { get; init; } = 30;
|
||||
public int MaxUses { get; init; } = 4;
|
||||
public Color ColorToApply { get; init; } = Color.GreenYellow;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record CompassConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 70;
|
||||
public override int Price { get; init; } = 60;
|
||||
public float MaxRange { get; init; } = 10000;
|
||||
public float CompassFOV { get; init; } = 120;
|
||||
public int CompassLength { get; init; } = 64;
|
||||
|
||||
24
TTT/ShopAPI/Configs/Traitor/TripwireConfig.cs
Normal file
24
TTT/ShopAPI/Configs/Traitor/TripwireConfig.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Drawing;
|
||||
|
||||
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);
|
||||
|
||||
public float TripwireSizeSquared { get; init; } = 500f;
|
||||
public Color TripwireColor { get; init; } = Color.FromArgb(64, Color.Red);
|
||||
public float TripwireThickness { get; init; } = 0.5f;
|
||||
|
||||
public TimeSpan DefuseTime { get; init; } = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan DefuseRate { get; init; } = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
public int DefuseReward { get; init; } = 20;
|
||||
}
|
||||
@@ -5,4 +5,5 @@ namespace ShopAPI;
|
||||
public interface IItemSorter {
|
||||
List<IShopItem> GetSortedItems(IOnlinePlayer? player, bool refresh = false);
|
||||
DateTime? GetLastUpdate(IOnlinePlayer? player);
|
||||
void InvalidateOrder(IOnlinePlayer? player);
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
11
TTT/SpecialRound/Events/SpecialRoundEnableEvent.cs
Normal file
11
TTT/SpecialRound/Events/SpecialRoundEnableEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using SpecialRoundAPI;
|
||||
|
||||
namespace SpecialRound.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a special round is enabled.
|
||||
/// Note that multiple special rounds may be enabled per round.
|
||||
/// </summary>
|
||||
/// <param name="round"></param>
|
||||
public class SpecialRoundEnableEvent(AbstractSpecialRound round)
|
||||
: SpecialRoundEvent(round);
|
||||
8
TTT/SpecialRound/Events/SpecialRoundEvent.cs
Normal file
8
TTT/SpecialRound/Events/SpecialRoundEvent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using SpecialRoundAPI;
|
||||
using TTT.API.Events;
|
||||
|
||||
namespace SpecialRound.Events;
|
||||
|
||||
public abstract class SpecialRoundEvent(AbstractSpecialRound round) : Event {
|
||||
public AbstractSpecialRound Round { get; } = round;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
|
||||
43
TTT/SpecialRound/Rounds/LowGravRound.cs
Normal file
43
TTT/SpecialRound/Rounds/LowGravRound.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class LowGravRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider) {
|
||||
private int originalGravity = 800;
|
||||
public override string Name => "Low Grav";
|
||||
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_LOWGRAV;
|
||||
public override SpecialRoundConfig Config => config;
|
||||
|
||||
private LowGravRoundConfig config
|
||||
=> Provider.GetService<IStorage<LowGravRoundConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new LowGravRoundConfig();
|
||||
|
||||
public override void ApplyRoundEffects() {
|
||||
var cvar = ConVar.Find("sv_gravity");
|
||||
if (cvar == null) return;
|
||||
|
||||
originalGravity = (int) Math.Round(cvar.GetPrimitiveValue<float>());
|
||||
var newGravity = (int)(originalGravity * config.GravityMultiplier);
|
||||
Server.NextWorldUpdate(()
|
||||
=> Server.ExecuteCommand($"sv_gravity {newGravity}"));
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
|
||||
Server.NextWorldUpdate(()
|
||||
=> Server.ExecuteCommand($"sv_gravity {originalGravity}"));
|
||||
}
|
||||
}
|
||||
66
TTT/SpecialRound/Rounds/PistolRound.cs
Normal file
66
TTT/SpecialRound/Rounds/PistolRound.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
50
TTT/SpecialRound/Rounds/RichRound.cs
Normal file
50
TTT/SpecialRound/Rounds/RichRound.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Events;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class RichRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider) {
|
||||
public override string Name => "Rich";
|
||||
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_RICH;
|
||||
public override SpecialRoundConfig Config => config;
|
||||
|
||||
private RichRoundConfig config
|
||||
=> Provider.GetService<IStorage<RichRoundConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new RichRoundConfig();
|
||||
|
||||
public override void ApplyRoundEffects() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnBalanceChange(PlayerBalanceEvent ev) {
|
||||
if (!Tracker.ActiveRounds.Contains(this)) return;
|
||||
if (ev.Reason == "Round Start") {
|
||||
var newBal = (int)(ev.NewBalance * config.BonusCreditsMultiplier);
|
||||
ev.NewBalance = newBal;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.NewBalance <= ev.OldBalance) return;
|
||||
|
||||
var gain = ev.NewBalance - ev.OldBalance;
|
||||
gain = (int)(gain * config.AdditiveCreditsMultiplier);
|
||||
ev.NewBalance = ev.OldBalance + gain;
|
||||
}
|
||||
|
||||
public override bool ConflictsWith(AbstractSpecialRound other) {
|
||||
return other is VanillaRound;
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) { }
|
||||
}
|
||||
30
TTT/SpecialRound/Rounds/SilentRound.cs
Normal file
30
TTT/SpecialRound/Rounds/SilentRound.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class SilentRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider) {
|
||||
public override string Name => "Silent";
|
||||
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_SILENT;
|
||||
public override SpecialRoundConfig Config => config;
|
||||
|
||||
private SilentRoundConfig config
|
||||
=> Provider.GetService<IStorage<SilentRoundConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new SilentRoundConfig();
|
||||
|
||||
public override void ApplyRoundEffects() {
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
player.VoiceFlags |= VoiceFlags.Muted;
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) { }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Role;
|
||||
@@ -58,6 +59,8 @@ public class SpeedRound(IServiceProvider provider)
|
||||
}
|
||||
|
||||
private void setTime(TimeSpan span) {
|
||||
span = TimeSpan.FromSeconds(Math.Min(span.TotalSeconds,
|
||||
config.MaxTimeEver.TotalSeconds));
|
||||
Server.NextWorldUpdate(() => {
|
||||
RoundUtil.SetTimeRemaining((int)span.TotalSeconds);
|
||||
});
|
||||
@@ -79,7 +82,7 @@ public class SpeedRound(IServiceProvider provider)
|
||||
public void OnDeath(PlayerDeathEvent ev) {
|
||||
var game = games.ActiveGame;
|
||||
if (game == null) return;
|
||||
if (Tracker.CurrentRound != this) return;
|
||||
if (!Tracker.ActiveRounds.Contains(this)) return;
|
||||
|
||||
var victimRoles = roles.GetRoles(ev.Victim);
|
||||
if (!victimRoles.Any(r => r is InnocentRole)) return;
|
||||
|
||||
48
TTT/SpecialRound/Rounds/SuppressedRound.cs
Normal file
48
TTT/SpecialRound/Rounds/SuppressedRound.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.UserMessages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class SuppressedRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider), IPluginModule {
|
||||
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");
|
||||
|
||||
if (!WeaponSoundIndex.PISTOLS.Contains(defIndex))
|
||||
return HookResult.Continue;
|
||||
|
||||
msg.Recipients.Clear();
|
||||
return HookResult.Handled;
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
plugin?.UnhookUserMessage(452, onWeaponSound);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Events;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Storage;
|
||||
@@ -34,10 +35,14 @@ public class VanillaRound(IServiceProvider provider)
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) { }
|
||||
|
||||
public override bool ConflictsWith(AbstractSpecialRound other) {
|
||||
return other is RichRound;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.HIGH)]
|
||||
public void OnPurchase(PlayerPurchaseItemEvent ev) {
|
||||
if (Tracker.CurrentRound != this) return;
|
||||
if (!Tracker.ActiveRounds.Contains(this)) return;
|
||||
ev.IsCanceled = true;
|
||||
|
||||
messenger.Message(ev.Player, locale[RoundMsgs.VANILLA_ROUND_REMINDER]);
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SpecialRoundAPI\SpecialRoundAPI\SpecialRoundAPI.csproj"/>
|
||||
<ProjectReference Include="..\API\API.csproj"/>
|
||||
<ProjectReference Include="..\CS2\CS2.csproj"/>
|
||||
<ProjectReference Include="..\Game\Game.csproj"/>
|
||||
<ProjectReference Include="..\SpecialRoundAPI\SpecialRoundAPI.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -9,9 +9,15 @@ public static class SpecialRoundCollection {
|
||||
public static void AddSpecialRounds(this IServiceCollection services) {
|
||||
services.AddModBehavior<ISpecialRoundStarter, SpecialRoundStarter>();
|
||||
services.AddModBehavior<ISpecialRoundTracker, SpecialRoundTracker>();
|
||||
services.AddModBehavior<SpecialRoundSoundNotifier>();
|
||||
|
||||
services.AddModBehavior<SpeedRound>();
|
||||
services.AddModBehavior<BhopRound>();
|
||||
services.AddModBehavior<LowGravRound>();
|
||||
services.AddModBehavior<PistolRound>();
|
||||
services.AddModBehavior<RichRound>();
|
||||
services.AddModBehavior<SilentRound>();
|
||||
services.AddModBehavior<SpeedRound>();
|
||||
services.AddModBehavior<SuppressedRound>();
|
||||
services.AddModBehavior<VanillaRound>();
|
||||
}
|
||||
}
|
||||
17
TTT/SpecialRound/SpecialRoundSoundNotifier.cs
Normal file
17
TTT/SpecialRound/SpecialRoundSoundNotifier.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using JetBrains.Annotations;
|
||||
using SpecialRound.Events;
|
||||
using TTT.API.Events;
|
||||
using TTT.Game.Listeners;
|
||||
|
||||
namespace SpecialRound;
|
||||
|
||||
public class SpecialRoundSoundNotifier(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnSpecialRoundStart(SpecialRoundEnableEvent ev) {
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
player.EmitSound("UI.XP.Star.Spend", null, 0.2f);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.Events;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using TTT.API;
|
||||
@@ -29,16 +30,22 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
plugin?.RegisterListener<Listeners.OnMapStart>(onMapChange);
|
||||
}
|
||||
|
||||
public AbstractSpecialRound?
|
||||
TryStartSpecialRound(AbstractSpecialRound? round) {
|
||||
round ??= getSpecialRound();
|
||||
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(round)]);
|
||||
Messenger.MessageAll(Locale[round.Description]);
|
||||
public List<AbstractSpecialRound> TryStartSpecialRound(
|
||||
List<AbstractSpecialRound>? rounds = null) {
|
||||
rounds ??= getSpecialRounds();
|
||||
|
||||
round?.ApplyRoundEffects();
|
||||
tracker.CurrentRound = round;
|
||||
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(rounds)]);
|
||||
|
||||
foreach (var round in rounds) {
|
||||
var roundStart = new SpecialRoundEnableEvent(round);
|
||||
Bus.Dispatch(roundStart);
|
||||
Messenger.MessageAll(Locale[round.Description]);
|
||||
round.ApplyRoundEffects();
|
||||
}
|
||||
|
||||
tracker.ActiveRounds.AddRange(rounds);
|
||||
tracker.RoundsSinceLastSpecial = 0;
|
||||
return round;
|
||||
return rounds;
|
||||
}
|
||||
|
||||
private void onMapChange(string mapName) { roundsSinceMapChange = 0; }
|
||||
@@ -57,16 +64,31 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
if (roundsSinceMapChange < config.MinRoundsAfterMapChange) return;
|
||||
if (Random.Shared.NextSingle() > config.SpecialRoundChance) return;
|
||||
|
||||
var specialRound = getSpecialRound();
|
||||
var specialRound = getSpecialRounds();
|
||||
|
||||
TryStartSpecialRound(specialRound);
|
||||
}
|
||||
|
||||
private AbstractSpecialRound getSpecialRound() {
|
||||
private List<AbstractSpecialRound> getSpecialRounds() {
|
||||
var selectedRounds = new List<AbstractSpecialRound>();
|
||||
|
||||
do {
|
||||
var round = pickWeightedRound(selectedRounds);
|
||||
if (round == null) break;
|
||||
selectedRounds.Add(round);
|
||||
} while (config.MultiRoundChance > Random.Shared.NextSingle());
|
||||
|
||||
return selectedRounds;
|
||||
}
|
||||
|
||||
private AbstractSpecialRound? pickWeightedRound(
|
||||
List<AbstractSpecialRound> exclude) {
|
||||
var rounds = Provider.GetServices<ITerrorModule>()
|
||||
.OfType<AbstractSpecialRound>()
|
||||
.Where(r => r.Config.Weight > 0)
|
||||
.Where(r => r.Config.Weight > 0 && !exclude.Contains(r))
|
||||
.Where(r => !exclude.Any(er => er.ConflictsWith(r) || r.ConflictsWith(er)))
|
||||
.ToList();
|
||||
if (rounds.Count == 0) return null;
|
||||
var totalWeight = rounds.Sum(r => r.Config.Weight);
|
||||
var roll = Random.Shared.NextDouble() * totalWeight;
|
||||
foreach (var round in rounds) {
|
||||
@@ -74,7 +96,6 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
if (roll <= 0) return round;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Failed to select a special round. This should never happen.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace SpecialRound;
|
||||
|
||||
public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
|
||||
IListener {
|
||||
public AbstractSpecialRound? CurrentRound { get; set; }
|
||||
public List<AbstractSpecialRound> ActiveRounds { get; } = new();
|
||||
public int RoundsSinceLastSpecial { get; set; }
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
@@ -18,6 +18,6 @@ public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
|
||||
[EventHandler]
|
||||
public void OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
CurrentRound = null;
|
||||
ActiveRounds.Clear();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
public record SpecialRoundsConfig {
|
||||
public int MinRoundsBetweenSpecial { get; init; } = 3;
|
||||
public int MinPlayersForSpecial { get; init; } = 8;
|
||||
public int MinPlayersForSpecial { get; init; } = 5;
|
||||
public int MinRoundsAfterMapChange { get; init; } = 2;
|
||||
public float SpecialRoundChance { get; init; } = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// If a special round is started, the chance that another special round
|
||||
/// will start in conjunction with it. This check is run until it fails,
|
||||
/// or we run out of special rounds to start.
|
||||
/// </summary>
|
||||
public float MultiRoundChance { get; init; } = 0.33f;
|
||||
}
|
||||
@@ -16,7 +16,25 @@ public class RoundMsgs {
|
||||
public static IMsg VANILLA_ROUND_REMINDER
|
||||
=> MsgFactory.Create(nameof(VANILLA_ROUND_REMINDER));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_STARTED(AbstractSpecialRound round) {
|
||||
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name);
|
||||
public static IMsg SPECIAL_ROUND_SUPPRESSED
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_SUPPRESSED));
|
||||
|
||||
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_RICH
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_RICH));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_LOWGRAV
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_LOWGRAV));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_STARTED(List<AbstractSpecialRound> round) {
|
||||
var roundNames = round.Count == 1 ?
|
||||
round[0].Name :
|
||||
string.Join(", ", round.Select(r => r.Name));
|
||||
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), roundNames);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,9 @@
|
||||
SPECIAL_ROUND_SPEED: " {yellow}SPEED{grey}: The round is faster than usual! {red}Traitors{grey} must kill to gain more time."
|
||||
SPECIAL_ROUND_BHOP: " {Yellow}BHOP{grey}: Bunny hopping is enabled! Hold jump to move faster!"
|
||||
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!"
|
||||
SPECIAL_ROUND_RICH: " {gold}RICH{grey}: All players start with extra credits!"
|
||||
SPECIAL_ROUND_LOWGRAV: " {lightblue}LOW GRAVITY{grey}: Players can jump higher and fall slower!"
|
||||
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."
|
||||
@@ -1,16 +1,15 @@
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Events;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public abstract class AbstractSpecialRound(IServiceProvider provider)
|
||||
: ITerrorModule, IListener {
|
||||
protected readonly IServiceProvider Provider = provider;
|
||||
|
||||
: BaseListener(provider) {
|
||||
protected readonly ISpecialRoundTracker Tracker =
|
||||
provider.GetRequiredService<ISpecialRoundTracker>();
|
||||
|
||||
@@ -18,11 +17,10 @@ public abstract class AbstractSpecialRound(IServiceProvider provider)
|
||||
public abstract IMsg Description { get; }
|
||||
public abstract SpecialRoundConfig Config { get; }
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public abstract void ApplyRoundEffects();
|
||||
|
||||
public virtual bool ConflictsWith(AbstractSpecialRound _) { return false; }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public abstract void OnGameState(GameStateUpdateEvent ev);
|
||||
5
TTT/SpecialRoundAPI/Configs/BhopRoundConfig.cs
Normal file
5
TTT/SpecialRoundAPI/Configs/BhopRoundConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record BhopRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.25f;
|
||||
}
|
||||
6
TTT/SpecialRoundAPI/Configs/LowGravRoundConfig.cs
Normal file
6
TTT/SpecialRoundAPI/Configs/LowGravRoundConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record LowGravRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.6f;
|
||||
public float GravityMultiplier { get; init; } = 0.5f;
|
||||
}
|
||||
5
TTT/SpecialRoundAPI/Configs/PistolRoundConfig.cs
Normal file
5
TTT/SpecialRoundAPI/Configs/PistolRoundConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record PistolRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.75f;
|
||||
}
|
||||
7
TTT/SpecialRoundAPI/Configs/RichRoundConfig.cs
Normal file
7
TTT/SpecialRoundAPI/Configs/RichRoundConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record RichRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.75f;
|
||||
public float BonusCreditsMultiplier { get; init; } = 2.0f;
|
||||
public float AdditiveCreditsMultiplier { get; init; } = 3.0f;
|
||||
}
|
||||
5
TTT/SpecialRoundAPI/Configs/SilentRoundConfig.cs
Normal file
5
TTT/SpecialRoundAPI/Configs/SilentRoundConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record SilentRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.5f;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace SpecialRoundAPI;
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public abstract record SpecialRoundConfig {
|
||||
public abstract float Weight { get; init; }
|
||||
@@ -1,8 +1,9 @@
|
||||
namespace SpecialRoundAPI;
|
||||
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);
|
||||
}
|
||||
5
TTT/SpecialRoundAPI/Configs/SuppressedRoundConfig.cs
Normal file
5
TTT/SpecialRoundAPI/Configs/SuppressedRoundConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record SuppressedRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.75f;
|
||||
}
|
||||
5
TTT/SpecialRoundAPI/Configs/VanillaRoundConfig.cs
Normal file
5
TTT/SpecialRoundAPI/Configs/VanillaRoundConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record VanillaRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.5f;
|
||||
}
|
||||
@@ -8,6 +8,6 @@ public interface ISpecialRoundStarter {
|
||||
/// </summary>
|
||||
/// <param name="round"></param>
|
||||
/// <returns></returns>
|
||||
public AbstractSpecialRound?
|
||||
TryStartSpecialRound(AbstractSpecialRound? round);
|
||||
public List<AbstractSpecialRound>? TryStartSpecialRound(
|
||||
List<AbstractSpecialRound>? round = null);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public interface ISpecialRoundTracker {
|
||||
public AbstractSpecialRound? CurrentRound { get; set; }
|
||||
public List<AbstractSpecialRound> ActiveRounds { get; }
|
||||
|
||||
public int RoundsSinceLastSpecial { get; set; }
|
||||
}
|
||||
16
TTT/SpecialRoundAPI/WeaponSoundIndex.cs
Normal file
16
TTT/SpecialRoundAPI/WeaponSoundIndex.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public class WeaponSoundIndex {
|
||||
public static readonly HashSet<uint> PISTOLS = [
|
||||
1, // deagle
|
||||
2, // dual berettas
|
||||
3, // five seven
|
||||
30, // tec9
|
||||
32, // p2000
|
||||
36, // p250
|
||||
4, // glock
|
||||
61, // usp-s
|
||||
63, // cz75 auto
|
||||
64
|
||||
];
|
||||
}
|
||||
44
TTT/Stats/SpecialRoundListener.cs
Normal file
44
TTT/Stats/SpecialRoundListener.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.Events;
|
||||
using SpecialRoundAPI;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Listeners;
|
||||
|
||||
namespace Stats;
|
||||
|
||||
public class SpecialRoundListener(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly HttpClient client =
|
||||
provider.GetRequiredService<HttpClient>();
|
||||
|
||||
private readonly IRoundTracker tracker =
|
||||
provider.GetRequiredService<IRoundTracker>();
|
||||
|
||||
private AbstractSpecialRound? round;
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnRoundStart(SpecialRoundEnableEvent ev) { round = ev.Round; }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
if (round == null) return;
|
||||
|
||||
var data = new { special_round = round.Name };
|
||||
var payload = new StringContent(JsonSerializer.Serialize(data),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
Task.Run(async () => {
|
||||
var id = tracker.CurrentRoundId;
|
||||
if (id == null) return;
|
||||
await client.PatchAsync("round/" + id.Value, payload);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Game\Game.csproj"/>
|
||||
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
|
||||
<ProjectReference Include="..\SpecialRound\SpecialRound.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -53,10 +53,10 @@ public class RoleAssignerTest(IServiceProvider provider) {
|
||||
[InlineData(9, 6, 2, 1)]
|
||||
[InlineData(10, 7, 2, 1)]
|
||||
[InlineData(20, 14, 4, 2)]
|
||||
[InlineData(30, 21, 6, 3)]
|
||||
[InlineData(32, 21, 7, 4)]
|
||||
[InlineData(60, 41, 12, 7)]
|
||||
[InlineData(64, 43, 13, 8)]
|
||||
[InlineData(30, 22, 6, 2)]
|
||||
[InlineData(32, 22, 7, 3)]
|
||||
[InlineData(60, 43, 12, 5)]
|
||||
[InlineData(64, 45, 13, 6)]
|
||||
public void AssignRole_AssignsBalanced_Roles(int players, int innos,
|
||||
int traitors, int detectives) {
|
||||
var playerList = new HashSet<IOnlinePlayer>();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user