Compare commits

...

24 Commits

Author SHA1 Message Date
MSWS
642155b1bc Revert "fix: Add check after roles assigned"
This reverts commit bacd288fe7.
2025-11-02 20:41:01 -08:00
MSWS
bacd288fe7 fix: Add check after roles assigned 2025-11-02 20:40:05 -08:00
MSWS
29e28038b8 update: Play poison sound only to player 2025-11-02 19:47:22 -08:00
MSWS
b253d8ee12 fix: InvalidateOrder not purging lastUpdate map 2025-11-02 01:23:31 -08:00
MSWS
02575b51e2 Make all rounds give 1 karma 2025-11-02 01:19:24 -08:00
MSWS
d8d365b497 update: Body paint price to match (30) 2025-11-02 01:17:27 -08:00
MSWS
1ac38dc0ad update: Reduce min requirement for special round 8 -> 5 2025-11-02 01:28:16 -07:00
MSWS
62e57ffa97 fix: Reduce karma grants per round 2025-11-02 01:22:06 -07:00
MSWS
81e6b2e695 revert: Remove unnecessary delay in MapHook 2025-11-02 01:18:13 -07:00
MSWS
2ce0457346 fix: Players identifying themselves (resolves #164) 2025-11-01 19:59:35 -07:00
MSWS
ed90c54e53 update: Update suppressed round to only suppress pistols 2025-11-01 19:58:13 -07:00
MSWS
06d2d71f76 feat: Add suppressed and silent rounds, close shop after purchase 2025-11-01 18:59:54 -07:00
MSWS
c6ba041a6b Adjust detective ratio 2025-11-01 18:19:40 -07:00
MSWS
f283d7407e update: Increase speedround limit 2025-11-01 17:56:10 -07:00
MSWS
51ff4df545 update: Reduce camouflage visibility 2025-11-01 17:42:10 -07:00
MSWS
e0ee4bf325 fix: Don't give glocks by default 2025-11-01 17:21:16 -07:00
MSWS
4a4c7e0782 Reduce cost of body paint 2025-11-01 17:00:41 -07:00
MSWS
d4f67ced0c Reduce cost of compasses 2025-11-01 16:41:12 -07:00
MSWS
33ca0c8385 update: Allow inspect button to be used alongside use 2025-10-31 20:10:46 -07:00
MSWS
ff2e97a3ce update: Don't play hurt sound if Traitor is shifting 2025-10-31 20:06:19 -07:00
MSWS
a56cdc1285 Reformat and Cleanup 2025-10-31 20:01:20 -07:00
MSWS
ceda5cba64 fix: Roles not being assigned on first round of map 2025-10-31 19:59:09 -07:00
MSWS
7c203bcd91 update: Increase prices of stickers and healthshot, impl CS2 healthshot config 2025-10-31 19:53:46 -07:00
MSWS
f91fc54897 update: Increase speedround weight +semver:patch 2025-10-31 18:41:07 -07:00
37 changed files with 242 additions and 56 deletions

View File

@@ -1,16 +1,16 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SpecialRoundAPI.Configs;
using TTT.API; using TTT.API;
using TTT.API.Events; using TTT.API.Events;
using TTT.Game.Events.Game; using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Locale; using TTT.Locale;
namespace SpecialRoundAPI; namespace SpecialRoundAPI;
public abstract class AbstractSpecialRound(IServiceProvider provider) public abstract class AbstractSpecialRound(IServiceProvider provider)
: ITerrorModule, IListener { : BaseListener(provider) {
protected readonly IServiceProvider Provider = provider;
protected readonly ISpecialRoundTracker Tracker = protected readonly ISpecialRoundTracker Tracker =
provider.GetRequiredService<ISpecialRoundTracker>(); provider.GetRequiredService<ISpecialRoundTracker>();
@@ -18,9 +18,6 @@ public abstract class AbstractSpecialRound(IServiceProvider provider)
public abstract IMsg Description { get; } public abstract IMsg Description { get; }
public abstract SpecialRoundConfig Config { get; } public abstract SpecialRoundConfig Config { get; }
public void Dispose() { }
public void Start() { }
public abstract void ApplyRoundEffects(); public abstract void ApplyRoundEffects();
[UsedImplicitly] [UsedImplicitly]

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
namespace SpecialRoundAPI; namespace SpecialRoundAPI.Configs;
public abstract record SpecialRoundConfig { public abstract record SpecialRoundConfig {
public abstract float Weight { get; init; } public abstract float Weight { get; init; }

View File

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

View File

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

View File

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

View File

@@ -1,5 +0,0 @@
namespace SpecialRoundAPI;
public record VanillaRoundConfig : SpecialRoundConfig {
public override float Weight { get; init; } = 0.3f;
}

View File

@@ -1,5 +1,6 @@
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs; using ShopAPI.Configs;
using ShopAPI.Configs.Detective; using ShopAPI.Configs.Detective;
using ShopAPI.Configs.Traitor; using ShopAPI.Configs.Traitor;
@@ -63,6 +64,8 @@ public static class CS2ServiceCollection {
collection collection
.AddModBehavior<IStorage<OneHitKnifeConfig>, CS2OneHitKnifeConfig>(); .AddModBehavior<IStorage<OneHitKnifeConfig>, CS2OneHitKnifeConfig>();
collection.AddModBehavior<IStorage<SilentAWPConfig>, CS2SilentAWPConfig>(); collection.AddModBehavior<IStorage<SilentAWPConfig>, CS2SilentAWPConfig>();
collection
.AddModBehavior<IStorage<HealthshotConfig>, CS2HealthshotConfig>();
// TTT - CS2 Specific optionals // TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>(); collection.AddScoped<ITextSpawner, TextSpawner>();

View File

@@ -41,15 +41,15 @@ public class PlayerPingShopAlias(IServiceProvider provider) : IPluginModule {
private void onButton(CCSPlayerController? player, int index) { private void onButton(CCSPlayerController? player, int index) {
if (player == null) return; 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 if (lastUpdated == null
|| DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20)) || DateTime.Now - lastUpdated > TimeSpan.FromSeconds(20))
return; return;
var cmdInfo = new CS2CommandInfo(provider, gamePlayer, 0, "css_shop", "buy", var cmdInfo = new CS2CommandInfo(provider, apiPlayer, 0, "css_shop", "buy",
(index - 1).ToString()); (index - 1).ToString()) { CallingContext = CommandCallingContext.Chat };
cmdInfo.CallingContext = CommandCallingContext.Chat;
provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo); provider.GetRequiredService<ICommandManager>().ProcessCommand(cmdInfo);
itemSorter.InvalidateOrder(apiPlayer);
} }
} }

View File

@@ -11,6 +11,8 @@ public class SpecCommand(IServiceProvider provider) : ICommand {
public void Dispose() { } public void Dispose() { }
public void Start() { } public void Start() { }
public string Id => "spec";
public Task<CommandResult> public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) { Execute(IOnlinePlayer? executor, ICommandInfo info) {
var target = executor; var target = executor;

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using SpecialRoundAPI; using SpecialRoundAPI;
using TTT.API; using TTT.API;
using TTT.API.Command; using TTT.API.Command;
@@ -25,7 +26,7 @@ public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
var rounds = provider.GetServices<ITerrorModule>() var rounds = provider.GetServices<ITerrorModule>()
.OfType<AbstractSpecialRound>() .OfType<AbstractSpecialRound>()
.ToDictionary(r => r.GetType().Name.ToLower(), r => r); .ToDictionary(r => r.Name.ToLower(), r => r);
var roundName = info.Args[1].ToLower(); var roundName = info.Args[1].ToLower();
if (!rounds.TryGetValue(roundName, out var round)) { if (!rounds.TryGetValue(roundName, out var round)) {
@@ -34,8 +35,10 @@ public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
return Task.FromResult(CommandResult.INVALID_ARGS); return Task.FromResult(CommandResult.INVALID_ARGS);
} }
tracker.TryStartSpecialRound(round); Server.NextWorldUpdate(() => {
info.ReplySync($"Started special round '{roundName}'."); tracker.TryStartSpecialRound(round);
info.ReplySync($"Started special round '{roundName}'.");
});
return Task.FromResult(CommandResult.SUCCESS); return Task.FromResult(CommandResult.SUCCESS);
} }
} }

View File

@@ -63,21 +63,19 @@ public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<string> CV_TRAITOR_WEAPONS = new( public static readonly FakeConVar<string> CV_TRAITOR_WEAPONS = new(
"css_ttt_roleweapons_traitor", "css_ttt_roleweapons_traitor",
"Weapons available to traitors at start of round", "Weapons available to traitors at start of round", "",
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE, ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<string> CV_DETECTIVE_WEAPONS = new( public static readonly FakeConVar<string> CV_DETECTIVE_WEAPONS = new(
"css_ttt_roleweapons_detective", "css_ttt_roleweapons_detective",
"Weapons available to detectives at start of round", "Weapons available to detectives at start of round",
"weapon_knife,weapon_taser,weapon_m4a1,weapon_revolver", "weapon_taser,weapon_m4a1_silencer,weapon_revolver", ConVarFlags.FCVAR_NONE,
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true)); new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<string> CV_INNOCENT_WEAPONS = new( public static readonly FakeConVar<string> CV_INNOCENT_WEAPONS = new(
"css_ttt_roleweapons_innocent", "css_ttt_roleweapons_innocent",
"Weapons available to innocents at start of round", "Weapons available to innocents at start of round", "",
"weapon_knife,weapon_glock", ConVarFlags.FCVAR_NONE, ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: true));
new ItemValidator(allowMultiple: true));
public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new( public static readonly FakeConVar<int> CV_TIME_BETWEEN_ROUNDS = new(
"css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1, "css_ttt_time_between_rounds", "Time to wait between rounds in seconds", 1,

View File

@@ -74,13 +74,13 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new( public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
"css_ttt_karma_per_round", "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)); ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 50));
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new( public static readonly FakeConVar<int> CV_KARMA_PER_ROUND_WIN = new(
"css_ttt_karma_per_round_win", "css_ttt_karma_per_round_win",
"Amount of karma a player will gain at the end of each round if their team won", "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() { } public void Dispose() { }

View File

@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2BodyPaintConfig : IStorage<BodyPaintConfig>, IPluginModule { public class CS2BodyPaintConfig : IStorage<BodyPaintConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new( 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)); ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_USES = new( public static readonly FakeConVar<int> CV_MAX_USES = new(

View File

@@ -16,7 +16,7 @@ public class CS2CamoConfig : IStorage<CamoConfig>, IPluginModule {
public static readonly FakeConVar<float> CV_CAMO_VISIBILITY = new( public static readonly FakeConVar<float> CV_CAMO_VISIBILITY = new(
"css_ttt_shop_camo_visibility", "css_ttt_shop_camo_visibility",
"Player visibility multiplier while camouflaged (0 = invisible, 1 = fully visible)", "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() { } public void Dispose() { }

View File

@@ -0,0 +1,43 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2HealthshotConfig : IStorage<HealthshotConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_healthshot_price", "Price of the Healthshot item", 40,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_PURCHASES = new(
"css_ttt_shop_healthshot_max_purchases",
"Maximum number of times a player can purchase the Healthshot per round", 2,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_healthshot_weapon", "Weapon entity name for the Healthshot",
"weapon_healthshot");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<HealthshotConfig?> Load() {
var cfg = new HealthshotConfig {
Price = CV_PRICE.Value,
MaxPurchases = CV_MAX_PURCHASES.Value,
Weapon = CV_WEAPON.Value
};
return Task.FromResult<HealthshotConfig?>(cfg);
}
}

View File

@@ -10,7 +10,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2StickersConfig : IStorage<StickersConfig>, IPluginModule { public class CS2StickersConfig : IStorage<StickersConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new( public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_stickers_price", "Price of the Stickers item (Detective)", 35, "css_ttt_shop_stickers_price", "Price of the Stickers item (Detective)", 45,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000)); ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public void Dispose() { } public void Dispose() { }

View File

@@ -51,7 +51,9 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
return; return;
} }
if (!pressed.HasFlag(PlayerButtons.Use)) return; if (!pressed.HasFlag(PlayerButtons.Use)
&& !pressed.HasFlag(PlayerButtons.Inspect))
return;
onStartUse(player); onStartUse(player);
} }

View File

@@ -1,4 +1,6 @@
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs.Traitor; using ShopAPI.Configs.Traitor;
@@ -45,6 +47,13 @@ public class DamageStation(IServiceProvider provider)
override protected void onInterval() { override protected void onInterval() {
var players = finder.GetOnline(); var players = finder.GetOnline();
var toRemove = new List<CPhysicsPropMultiplayer>(); var toRemove = new List<CPhysicsPropMultiplayer>();
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();
foreach (var (prop, info) in props) { foreach (var (prop, info) in props) {
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven) if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
> Math.Abs(_Config.TotalHealthGiven)) { > Math.Abs(_Config.TotalHealthGiven)) {
@@ -59,10 +68,6 @@ public class DamageStation(IServiceProvider provider)
var propPos = prop.AbsOrigin; var propPos = prop.AbsOrigin;
var playerMapping = players.Select(p
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
.Where(m => m.GamePlayer != null);
var playerDists = playerMapping var playerDists = playerMapping
.Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin, .Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin,
t.GamePlayer)) t.GamePlayer))
@@ -73,9 +78,6 @@ public class DamageStation(IServiceProvider provider)
.ToList(); .ToList();
foreach (var (player, dist, gamePlayer) in playerDists) { 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 healthScale = 1.0 - dist / _Config.MaxRange;
var damageAmount = var damageAmount =
(int)Math.Floor(_Config.HealthIncrements * healthScale); (int)Math.Floor(_Config.HealthIncrements * healthScale);
@@ -95,6 +97,7 @@ public class DamageStation(IServiceProvider provider)
bus.Dispatch(playerDeath); bus.Dispatch(playerDeath);
} }
gamePlayer.EmitSound("Player.DamageFall", SELF(gamePlayer.Slot), 0.2f);
player.Health += damageAmount; player.Health += damageAmount;
info.HealthGiven += damageAmount; info.HealthGiven += damageAmount;
} }
@@ -103,6 +106,10 @@ public class DamageStation(IServiceProvider provider)
foreach (var prop in toRemove) props.Remove(prop); foreach (var prop in toRemove) props.Remove(prop);
} }
private static RecipientFilter SELF(int slot) {
return new RecipientFilter(slot);
}
[UsedImplicitly] [UsedImplicitly]
[EventHandler] [EventHandler]
public void OnGameEnd(GameStateUpdateEvent ev) { public void OnGameEnd(GameStateUpdateEvent ev) {

View File

@@ -34,6 +34,8 @@ public class BodyPickupListener(IServiceProvider provider)
if (ev.Player is not IOnlinePlayer online) if (ev.Player is not IOnlinePlayer online)
throw new InvalidOperationException("Player is not an online player."); throw new InvalidOperationException("Player is not an online player.");
if (ev.Player.Id == body.OfPlayer.Id) return;
var identifyEvent = new BodyIdentifyEvent(body, online); var identifyEvent = new BodyIdentifyEvent(body, online);
Bus.Dispatch(identifyEvent); Bus.Dispatch(identifyEvent);

View File

@@ -9,11 +9,12 @@ public record TTTConfig {
public Func<int, int> TraitorCount { get; init; } = public Func<int, int> TraitorCount { get; init; } =
p => (int)Math.Ceiling((p - 1) / 5f); p => (int)Math.Ceiling((p - 1) / 5f);
public Func<int, int> DetectiveCount { get; init; } = public Func<int, int> DetectiveCount { get; init; } = p
p => (int)Math.Floor(p / 8f); => (int)Math.Ceiling(Math.Floor(p / 8f) / 1.5f);
public Func<int, int> InnocentCount { get; init; } = p 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 record RoleConfig {

View File

@@ -74,6 +74,12 @@ public class ListCommand(IServiceProvider provider) : ICommand, IItemSorter {
return time; 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) { private List<IShopItem> calculateSortedItems(IOnlinePlayer? player) {
var items = new List<IShopItem>(shop.Items).Where(item var items = new List<IShopItem>(shop.Items).Where(item
=> player == null => player == null

View File

@@ -72,4 +72,8 @@ public class ShopCommand(IServiceProvider provider) : ICommand, IItemSorter {
public DateTime? GetLastUpdate(IOnlinePlayer? player) { public DateTime? GetLastUpdate(IOnlinePlayer? player) {
return listCmd.GetLastUpdate(player); return listCmd.GetLastUpdate(player);
} }
public void InvalidateOrder(IOnlinePlayer? player) {
listCmd.InvalidateOrder(player);
}
} }

View File

@@ -3,7 +3,7 @@ using System.Drawing;
namespace ShopAPI.Configs; namespace ShopAPI.Configs;
public record BodyPaintConfig : ShopItemConfig { 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 int MaxUses { get; init; } = 4;
public Color ColorToApply { get; init; } = Color.GreenYellow; public Color ColorToApply { get; init; } = Color.GreenYellow;
} }

View File

@@ -1,7 +1,7 @@
namespace ShopAPI.Configs.Traitor; namespace ShopAPI.Configs.Traitor;
public record CompassConfig : ShopItemConfig { 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 MaxRange { get; init; } = 10000;
public float CompassFOV { get; init; } = 120; public float CompassFOV { get; init; } = 120;
public int CompassLength { get; init; } = 64; public int CompassLength { get; init; } = 64;

View File

@@ -5,4 +5,5 @@ namespace ShopAPI;
public interface IItemSorter { public interface IItemSorter {
List<IShopItem> GetSortedItems(IOnlinePlayer? player, bool refresh = false); List<IShopItem> GetSortedItems(IOnlinePlayer? player, bool refresh = false);
DateTime? GetLastUpdate(IOnlinePlayer? player); DateTime? GetLastUpdate(IOnlinePlayer? player);
void InvalidateOrder(IOnlinePlayer? player);
} }

View File

@@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SpecialRound.lang; using SpecialRound.lang;
using SpecialRoundAPI; using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API.Game; using TTT.API.Game;
using TTT.API.Storage; using TTT.API.Storage;
using TTT.Game.Events.Game; using TTT.Game.Events.Game;

View 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) { }
}

View File

@@ -4,6 +4,7 @@ using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using SpecialRound.lang; using SpecialRound.lang;
using SpecialRoundAPI; using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API.Events; using TTT.API.Events;
using TTT.API.Game; using TTT.API.Game;
using TTT.API.Role; using TTT.API.Role;
@@ -58,6 +59,8 @@ public class SpeedRound(IServiceProvider provider)
} }
private void setTime(TimeSpan span) { private void setTime(TimeSpan span) {
span = TimeSpan.FromSeconds(Math.Min(span.TotalSeconds,
config.MaxTimeEver.TotalSeconds));
Server.NextWorldUpdate(() => { Server.NextWorldUpdate(() => {
RoundUtil.SetTimeRemaining((int)span.TotalSeconds); RoundUtil.SetTimeRemaining((int)span.TotalSeconds);
}); });

View File

@@ -0,0 +1,61 @@
using CounterStrikeSharp.API;
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 {
public override string Name => "Suppressed";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_SUPPRESSED;
public override SpecialRoundConfig Config => config;
private BasePlugin? plugin;
private SuppressedRoundConfig config
=> Provider.GetService<IStorage<SuppressedRoundConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new SuppressedRoundConfig();
public void Start(BasePlugin? newPlugin) { plugin ??= newPlugin; }
public override void ApplyRoundEffects() {
plugin?.HookUserMessage(452, onWeaponSound);
}
private static readonly HashSet<uint> silencedWeapons = new() {
1, // deagle
2, // dual berettas
3, // five seven
30, // tec9
32, // p2000
36, // p250
4, // glock
61, // usp-s
63, // cz75 auto
64 // r8 revolver
};
private HookResult onWeaponSound(UserMessage msg) {
var defIndex = msg.ReadUInt("item_def_index");
if (!silencedWeapons.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);
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Events; using ShopAPI.Events;
using SpecialRound.lang; using SpecialRound.lang;
using SpecialRoundAPI; using SpecialRoundAPI;
using SpecialRoundAPI.Configs;
using TTT.API.Events; using TTT.API.Events;
using TTT.API.Messages; using TTT.API.Messages;
using TTT.API.Storage; using TTT.API.Storage;

View File

@@ -13,5 +13,7 @@ public static class SpecialRoundCollection {
services.AddModBehavior<SpeedRound>(); services.AddModBehavior<SpeedRound>();
services.AddModBehavior<BhopRound>(); services.AddModBehavior<BhopRound>();
services.AddModBehavior<VanillaRound>(); services.AddModBehavior<VanillaRound>();
services.AddModBehavior<SuppressedRound>();
services.AddModBehavior<SilentRound>();
} }
} }

View File

@@ -2,7 +2,7 @@
public record SpecialRoundsConfig { public record SpecialRoundsConfig {
public int MinRoundsBetweenSpecial { get; init; } = 3; 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 int MinRoundsAfterMapChange { get; init; } = 2;
public float SpecialRoundChance { get; init; } = 0.2f; public float SpecialRoundChance { get; init; } = 0.2f;
} }

View File

@@ -16,6 +16,12 @@ public class RoundMsgs {
public static IMsg VANILLA_ROUND_REMINDER public static IMsg VANILLA_ROUND_REMINDER
=> MsgFactory.Create(nameof(VANILLA_ROUND_REMINDER)); => MsgFactory.Create(nameof(VANILLA_ROUND_REMINDER));
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_STARTED(AbstractSpecialRound round) { public static IMsg SPECIAL_ROUND_STARTED(AbstractSpecialRound round) {
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name); return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name);
} }

View File

@@ -2,4 +2,6 @@
SPECIAL_ROUND_SPEED: " {yellow}SPEED{grey}: The round is faster than usual! {red}Traitors{grey} must kill to gain more time." 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_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_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!"
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled." VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."

View File

@@ -53,10 +53,10 @@ public class RoleAssignerTest(IServiceProvider provider) {
[InlineData(9, 6, 2, 1)] [InlineData(9, 6, 2, 1)]
[InlineData(10, 7, 2, 1)] [InlineData(10, 7, 2, 1)]
[InlineData(20, 14, 4, 2)] [InlineData(20, 14, 4, 2)]
[InlineData(30, 21, 6, 3)] [InlineData(30, 22, 6, 2)]
[InlineData(32, 21, 7, 4)] [InlineData(32, 22, 7, 3)]
[InlineData(60, 41, 12, 7)] [InlineData(60, 43, 12, 5)]
[InlineData(64, 43, 13, 8)] [InlineData(64, 45, 13, 6)]
public void AssignRole_AssignsBalanced_Roles(int players, int innos, public void AssignRole_AssignsBalanced_Roles(int players, int innos,
int traitors, int detectives) { int traitors, int detectives) {
var playerList = new HashSet<IOnlinePlayer>(); var playerList = new HashSet<IOnlinePlayer>();