Compare commits

...

17 Commits

Author SHA1 Message Date
MSWS
daa24a0e87 fix: Allow typing if dead even with muted roll 2025-10-28 19:20:42 -07:00
MSWS
1c8785b388 Add reminder for vanilla rounds 2025-10-28 18:50:09 -07:00
MSWS
a80c36e3c5 Suppress damage stats 2025-10-28 15:29:10 -07:00
MSWS
ba6b6c448f Change currency name to point 2025-10-28 14:06:29 -07:00
MSWS
d30e916319 Fix role name cleaning 2025-10-28 13:47:43 -07:00
MSWS
9f275fa189 Slight tweaks to logging 2025-10-28 13:39:02 -07:00
MSWS
40bdcac4b0 fix: Address copilot concerns 2025-10-28 13:30:19 -07:00
MSWS
6b3ae03ab3 Require both roles for BAD ACTION 2025-10-28 13:23:37 -07:00
MSWS
9e8e1d1fb0 Add prefix to death action 2025-10-28 13:22:53 -07:00
MSWS
fba875f098 Use prefix 2025-10-28 13:21:40 -07:00
MSWS
38fa801c15 feat: Teleport decoy working (resolves #153) 2025-10-28 13:19:29 -07:00
MSWS
b21fed3ff8 Start work on teleport decoy 2025-10-28 13:03:12 -07:00
MSWS
0e02d66350 Add configs 2025-10-28 12:39:37 -07:00
MSWS
7f6ac62348 fix: Karma events not dispatching 2025-10-28 12:28:26 -07:00
MSWS
f44e57215f fix: Special Round weights 2025-10-28 12:15:42 -07:00
MSWS
ce48f5a5ac Add logs for RTD as well 2025-10-28 12:11:54 -07:00
MSWS
4f258e55dd Log tasers 2025-10-28 11:45:43 -07:00
50 changed files with 711 additions and 176 deletions

View File

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

View File

@@ -11,6 +11,7 @@ public interface IAction {
string Id { get; }
string Verb { get; }
string Details { get; }
string Prefix => "";
public string Format() {
var pRole = PlayerRole != null ?
@@ -20,7 +21,7 @@ public interface IAction {
$" [{OtherRole.Name.First(char.IsAsciiLetter)}]" :
"";
return Other is not null ?
$"{Player}{pRole} {Verb} {Other}{oRole} {Details}" :
$"{Player}{pRole} {Verb} {Details}";
$"{Prefix}{Player}{pRole} {Verb} {Other}{oRole} {Details}" :
$"{Prefix}{Player}{pRole} {Verb} {Details}";
}
}

View File

@@ -0,0 +1,19 @@
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
namespace TTT.CS2.Actions;
public class TaserAction(IRoleAssigner roles, IPlayer victim,
IPlayer identifier) : IAction {
public IPlayer Player { get; } = identifier;
public IPlayer? Other { get; } = victim;
public IRole? PlayerRole { get; } =
roles.GetRoles(identifier).FirstOrDefault();
public IRole? OtherRole { get; } = roles.GetRoles(victim).FirstOrDefault();
public string Id { get; } = "cs2.action.tased";
public string Verb { get; } = "tased";
public string Details { get; } = "";
}

View File

@@ -1,6 +1,8 @@
using System.Runtime.CompilerServices;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
using ShopAPI.Configs.Traitor;
using TTT.API.Command;
using TTT.API.Extensions;
@@ -50,6 +52,18 @@ public static class CS2ServiceCollection {
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
collection.AddModBehavior<IStorage<KarmaConfig>, CS2KarmaConfig>();
collection.AddModBehavior<IStorage<CamoConfig>, CS2CamoConfig>();
collection.AddModBehavior<IStorage<StickersConfig>, CS2StickersConfig>();
collection.AddModBehavior<IStorage<BodyPaintConfig>, CS2BodyPaintConfig>();
collection
.AddModBehavior<IStorage<DnaScannerConfig>, CS2DnaScannerConfig>();
collection
.AddModBehavior<IStorage<HealthStationConfig>, CS2HealthStationConfig>();
collection
.AddModBehavior<IStorage<ClusterGrenadeConfig>, CS2ClusterGrenadeConfig>();
collection.AddModBehavior<IStorage<GlovesConfig>, CS2GlovesConfig>();
collection
.AddModBehavior<IStorage<OneHitKnifeConfig>, CS2OneHitKnifeConfig>();
collection.AddModBehavior<IStorage<SilentAWPConfig>, CS2SilentAWPConfig>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
@@ -68,7 +82,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<TraitorChatHandler>();
collection.AddModBehavior<PlayerMuter>();
collection.AddModBehavior<MapChangeCausesEndListener>();
collection.AddModBehavior<EntityTargetHandlers>();
// collection.AddModBehavior<EntityTargetHandlers>();
// Damage Cancelers
collection.AddModBehavior<OutOfRoundCanceler>();

View File

@@ -20,16 +20,10 @@ public class SetTargetCommand(IServiceProvider provider) : ICommand {
public string Id => "settarget";
private static readonly Vector RELAY_POSITION = new(69, 420, -60);
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
var name = "TRAITOR";
if (info.ArgCount == 2) name = info.Args[1];
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(executor);
if (gamePlayer == null) return;

View File

@@ -0,0 +1,52 @@
using System.Drawing;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
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,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_USES = new(
"css_ttt_shop_bodypaint_max_uses",
"Maximum number of times the Body Paint can be applied per item", 4,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public static readonly FakeConVar<string> CV_COLOR = new(
"css_ttt_shop_bodypaint_color",
"Color to apply to the player's body (HTML hex or known color name)",
"GreenYellow", ConVarFlags.FCVAR_NONE);
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<BodyPaintConfig?> Load() {
Color parsedColor;
try { parsedColor = ColorTranslator.FromHtml(CV_COLOR.Value); } catch {
try { parsedColor = Color.FromName(CV_COLOR.Value); } catch {
parsedColor = Color.GreenYellow;
}
}
var cfg = new BodyPaintConfig {
Price = CV_PRICE.Value,
MaxUses = CV_MAX_USES.Value,
ColorToApply = parsedColor
};
return Task.FromResult<BodyPaintConfig?>(cfg);
}
}

View File

@@ -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.4f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
0.6f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1f));
public void Dispose() { }

View File

@@ -0,0 +1,57 @@
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 CS2ClusterGrenadeConfig : IStorage<ClusterGrenadeConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_clustergrenade_price",
"Price of the Cluster Grenade item (Traitor)", 90, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_GRENADE_COUNT = new(
"css_ttt_shop_clustergrenade_count",
"Number of grenades released upon explosion", 8, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 50));
public static readonly FakeConVar<string> CV_WEAPON_ID = new(
"css_ttt_shop_clustergrenade_weapon",
"Weapon entity ID used for the Cluster Grenade", "weapon_hegrenade",
ConVarFlags.FCVAR_NONE);
public static readonly FakeConVar<float> CV_UP_FORCE = new(
"css_ttt_shop_clustergrenade_up_force",
"Upward force applied to cluster fragments", 200f, ConVarFlags.FCVAR_NONE,
new RangeValidator<float>(0f, 1000f));
public static readonly FakeConVar<float> CV_THROW_FORCE = new(
"css_ttt_shop_clustergrenade_throw_force",
"Forward throw force applied to cluster fragments", 250f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 1000f));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<ClusterGrenadeConfig?> Load() {
var cfg = new ClusterGrenadeConfig {
Price = CV_PRICE.Value,
GrenadeCount = CV_GRENADE_COUNT.Value,
UpForce = CV_UP_FORCE.Value,
ThrowForce = CV_THROW_FORCE.Value
};
return Task.FromResult<ClusterGrenadeConfig?>(cfg);
}
}

View File

@@ -0,0 +1,44 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2DnaScannerConfig : IStorage<DnaScannerConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_dna_price", "Price of the DNA Scanner item (Detective)", 110,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_SAMPLES = new(
"css_ttt_shop_dna_max_samples",
"Maximum number of DNA samples that can be stored at once", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
public static readonly FakeConVar<int> CV_DECAY_TIME_SECONDS = new(
"css_ttt_shop_dna_decay_time",
"Time (in seconds) before a DNA sample decays", 120, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(10, 3600));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<DnaScannerConfig?> Load() {
var cfg = new DnaScannerConfig {
Price = CV_PRICE.Value,
MaxSamples = CV_MAX_SAMPLES.Value,
DecayTime = TimeSpan.FromSeconds(CV_DECAY_TIME_SECONDS.Value)
};
return Task.FromResult<DnaScannerConfig?>(cfg);
}
}

View File

@@ -0,0 +1,37 @@
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 CS2GlovesConfig : IStorage<GlovesConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_gloves_price", "Price of the Gloves item (Traitor)", 40,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_MAX_USES = new(
"css_ttt_shop_gloves_max_uses",
"Maximum number of times the Gloves can be used before breaking", 5,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 100));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<GlovesConfig?> Load() {
var cfg = new GlovesConfig {
Price = CV_PRICE.Value, MaxUses = CV_MAX_USES.Value
};
return Task.FromResult<GlovesConfig?>(cfg);
}
}

View File

@@ -0,0 +1,70 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2HealthStationConfig : IStorage<HealthStationConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_healthstation_price",
"Price of the Health Station item (Detective)", 50, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_USE_SOUND = new(
"css_ttt_shop_healthstation_use_sound",
"Sound played when using the Health Station", "sounds/buttons/blip1",
ConVarFlags.FCVAR_NONE);
public static readonly FakeConVar<int> CV_HEALTH_INCREMENTS = new(
"css_ttt_shop_healthstation_increments",
"Number of health increments applied per use", 10, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(1, 100));
public static readonly FakeConVar<int> CV_HEALTH_INTERVAL = new(
"css_ttt_shop_healthstation_interval",
"Interval (in seconds) between health increments", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 60));
public static readonly FakeConVar<int> CV_STATION_HEALTH = new(
"css_ttt_shop_healthstation_station_health",
"Maximum health of the station object itself", 200, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(50, 1000));
public static readonly FakeConVar<int> CV_TOTAL_HEALTH_GIVEN = new(
"css_ttt_shop_healthstation_total_health_given",
"Total health the station can provide before depleting (0 = infinite)", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<float> CV_MAX_RANGE = new(
"css_ttt_shop_healthstation_max_range",
"Maximum range (in units) from which players can use the station", 256f,
ConVarFlags.FCVAR_NONE, new RangeValidator<float>(50f, 2048f));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<HealthStationConfig?> Load() {
var cfg = new HealthStationConfig {
Price = CV_PRICE.Value,
UseSound = CV_USE_SOUND.Value,
HealthIncrements = CV_HEALTH_INCREMENTS.Value,
HealthInterval = TimeSpan.FromSeconds(CV_HEALTH_INTERVAL.Value),
StationHealth = CV_STATION_HEALTH.Value,
TotalHealthGiven = CV_TOTAL_HEALTH_GIVEN.Value,
MaxRange = CV_MAX_RANGE.Value
};
return Task.FromResult<HealthStationConfig?>(cfg);
}
}

View File

@@ -0,0 +1,38 @@
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 CS2OneHitKnifeConfig : IStorage<OneHitKnifeConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onehitknife_price",
"Price of the One-Hit Knife item (Traitor)", 80, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_onehitknife_friendly_fire",
"Whether the One-Hit Knife can damage teammates", false,
ConVarFlags.FCVAR_NONE);
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<OneHitKnifeConfig?> Load() {
var cfg = new OneHitKnifeConfig {
Price = CV_PRICE.Value, FriendlyFire = CV_FRIENDLY_FIRE.Value
};
return Task.FromResult<OneHitKnifeConfig?>(cfg);
}
}

View File

@@ -12,7 +12,7 @@ namespace TTT.CS2.Configs.ShopItems;
public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 110,
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 125,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(

View File

@@ -0,0 +1,54 @@
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 CS2SilentAWPConfig : IStorage<SilentAWPConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_silentawp_price", "Price of the Silent AWP item (Traitor)",
80, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<int> CV_WEAPON_INDEX = new(
"css_ttt_shop_silentawp_index", "Weapon slot index for the Silent AWP", 9,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<string> CV_WEAPON_ID = new(
"css_ttt_shop_silentawp_weapon", "Weapon entity ID for the Silent AWP",
"weapon_awp", ConVarFlags.FCVAR_NONE);
public static readonly FakeConVar<int> CV_RESERVE_AMMO = new(
"css_ttt_shop_silentawp_reserve_ammo",
"Reserve ammo count for the Silent AWP", 0, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 100));
public static readonly FakeConVar<int> CV_CURRENT_AMMO = new(
"css_ttt_shop_silentawp_current_ammo",
"Current ammo loaded in the Silent AWP", 1, ConVarFlags.FCVAR_NONE,
new RangeValidator<int>(0, 10));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<SilentAWPConfig?> Load() {
var cfg = new SilentAWPConfig {
Price = CV_PRICE.Value,
WeaponIndex = CV_WEAPON_INDEX.Value,
WeaponId = CV_WEAPON_ID.Value,
ReserveAmmo = CV_RESERVE_AMMO.Value,
CurrentAmmo = CV_CURRENT_AMMO.Value
};
return Task.FromResult<SilentAWPConfig?>(cfg);
}
}

View File

@@ -0,0 +1,30 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs.Detective;
using TTT.API;
using TTT.API.Storage;
namespace TTT.CS2.Configs.ShopItems;
public class CS2StickersConfig : IStorage<StickersConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_stickers_price", "Price of the Stickers item (Detective)", 25,
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<StickersConfig?> Load() {
var cfg = new StickersConfig { Price = CV_PRICE.Value };
return Task.FromResult<StickersConfig?>(cfg);
}
}

View File

@@ -7,7 +7,7 @@ using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs;
namespace TTT.CS2.Configs.ShopItems;
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(

View File

@@ -1,4 +1,5 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Commands;
using JetBrains.Annotations;
@@ -29,9 +30,9 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
{ "weapon_usp_silencer", "M4A1" },
{ "weapon_sg556", "M4A1" },
{ "weapon_mp5sd", "M4A1" },
{ "weapon_decoy", "healthshot" },
{ "weapon_awp", "AWP" },
{ "weapon_hegrenade", "Cluster" }
{ "weapon_hegrenade", "Cluster" },
{ "weapon_decoy", "Teleport Decoy" },
};
public void Dispose() { }
@@ -49,6 +50,14 @@ public class BuyMenuHandler(IServiceProvider provider) : IPluginModule {
}
inventory.RemoveWeapon(player, new BaseWeapon(ev.Weapon));
switch (ev.Weapon) {
case "weapon_m4a1_silencer":
inventory.RemoveWeaponInSlot(player, 0);
break;
case "weapon_revolver":
inventory.RemoveWeaponInSlot(player, 1);
break;
}
if (!shopAliases.TryGetValue(ev.Weapon, out var alias))
return HookResult.Continue;

View File

@@ -52,6 +52,17 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler(HookMode.Pre)]
public HookResult OnPlayerDamage(EventPlayerHurt ev, GameEventInfo info) {
var player = ev.Userid;
if (player == null) return HookResult.Continue;
hideAndTrackStats(ev);
return HookResult.Continue;
}
private void hideAndTrackStats(EventPlayerDeath ev,
CCSPlayerController player) {
var victimStats = player.ActionTrackingServices?.MatchStats;
@@ -89,6 +100,16 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
"m_pActionTrackingServices");
}
private void hideAndTrackStats(EventPlayerHurt ev) {
var attackerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
if (attackerStats == null) return;
if (ev.Attacker == null) return;
attackerStats.Damage -= ev.DmgHealth;
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
"m_pActionTrackingServices");
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnPlayerHurt(EventPlayerHurt ev, GameEventInfo _) {

View File

@@ -1,5 +1,7 @@
using TTT.API.Events;
using JetBrains.Annotations;
using TTT.API.Events;
using TTT.API.Game;
using TTT.CS2.Actions;
using TTT.CS2.lang;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
@@ -8,6 +10,7 @@ namespace TTT.CS2.GameHandlers.DamageCancelers;
public class TaserListenCanceler(IServiceProvider provider)
: BaseListener(provider) {
[UsedImplicitly]
[EventHandler]
public void OnHurt(PlayerDamagedEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
@@ -23,5 +26,6 @@ public class TaserListenCanceler(IServiceProvider provider)
Messenger.Message(attacker,
Locale[CS2Msgs.TASER_SCANNED(victim, Roles.GetRoles(victim).First())]);
Games.ActiveGame.Logger.LogAction(new TaserAction(Roles, victim, attacker));
}
}

View File

@@ -1,64 +0,0 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
namespace TTT.CS2.GameHandlers;
public class EntityTargetHandlers(IServiceProvider provider) : IPluginModule {
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
plugin?.HookEntityOutput("*", "*", handler);
}
private HookResult handler(CEntityIOOutput output, string name,
CEntityInstance activator, CEntityInstance caller, CVariant value,
float delay) {
if (caller.DesignerName == "prop_dynamic") return HookResult.Continue;
messenger.Debug("Entity Output Triggered: " + name);
messenger.Debug("Activator: " + activator.DesignerName);
messenger.Debug("Caller: " + caller.DesignerName);
messenger.Debug("Value: " + value + " " + value.GetType());
caller.AcceptInput("OnPass");
activator.AcceptInput("OnPass");
if (caller.DesignerName != "filter_activator_name")
return HookResult.Continue;
var csPlayer =
Utilities.GetPlayerFromIndex((int)activator.EntityHandle.Index);
if (csPlayer != null && csPlayer.IsValid) {
messenger.DebugAnnounce(
$"Filter Activator Name triggered by player: {csPlayer.PlayerName} {(int)csPlayer.Index}");
}
var ptrPlayer = new CCSPlayerController(activator.Handle);
if (ptrPlayer.IsValid) {
messenger.DebugAnnounce(
$"Filter Activator Name triggered by player controller: {ptrPlayer.PlayerName} {(int)ptrPlayer.Index}");
}
messenger.DebugAnnounce(output + " - " + output.Description);
var connections = output.Connections;
if (connections != null) debugConnection(connections);
caller.AcceptInput("OnPass");
return HookResult.Continue;
}
private void debugConnection(EntityIOConnection_t connection) {
messenger.DebugAnnounce("Connection:");
messenger.DebugAnnounce(" Target: " + connection.Target);
messenger.DebugAnnounce(" Input: " + connection.TargetInput);
messenger.DebugAnnounce(" Parameter: " + connection.ValueOverride);
messenger.DebugAnnounce(" Delay: " + connection.Delay);
messenger.DebugAnnounce(" Times to fire: " + connection.TimesToFire);
if (connection.Next != null) debugConnection(connection.Next);
}
}

View File

@@ -79,7 +79,7 @@ public class TraitorChatHandler(IServiceProvider provider) : IPluginModule {
private HookResult onSay(CCSPlayerController? player,
CommandInfo commandInfo) {
if (mutedPlayers != null
if (mutedPlayers != null && player != null && player.GetHealth() > 0
&& mutedPlayers.Contains(player?.SteamID.ToString() ?? ""))
return HookResult.Handled;

View File

@@ -10,7 +10,7 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonShots;
public static class PoisonShotServiceCollection {
public static void AddPoisonShots(this IServiceCollection services) {
public static void AddPoisonShotsServices(this IServiceCollection services) {
services.AddModBehavior<PoisonShotsItem>();
services.AddModBehavior<PoisonShotsListener>();
}

View File

@@ -10,7 +10,7 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.PoisonSmoke;
public static class PoisonSmokeServiceCollection {
public static void AddPoisonSmoke(this IServiceCollection services) {
public static void AddPoisonSmokeServices(this IServiceCollection services) {
services.AddModBehavior<PoisonSmokeItem>();
services.AddModBehavior<PoisonSmokeListener>();
}

View File

@@ -10,7 +10,7 @@ using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
public static class HealthStationCollection {
public static void AddHealthStation(this IServiceCollection collection) {
public static void AddHealthStationServices(this IServiceCollection collection) {
collection.AddModBehavior<HealthStation>();
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Roles;
namespace TTT.CS2.Items.TeleportDecoy;
public static class TeleportDecoyServiceCollection {
public static void AddTeleportDecoyServices(
this IServiceCollection collection) {
collection.AddModBehavior<TeleportDecoyItem>();
collection.AddModBehavior<TeleportDecoyListener>();
}
}
public class TeleportDecoyItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
public override string Name
=> Locale[TeleportDecoyMsgs.SHOP_ITEM_TELEPORT_DECOY];
public override string Description
=> Locale[TeleportDecoyMsgs.SHOP_ITEM_TELEPORT_DECOY_DESC];
private TeleportDecoyConfig config
=> Provider.GetService<IStorage<TeleportDecoyConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new TeleportDecoyConfig();
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Inventory.GiveWeapon(player, new BaseWeapon("weapon_decoy"));
}
}

View File

@@ -0,0 +1,37 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API;
using TTT.API.Player;
namespace TTT.CS2.Items.TeleportDecoy;
public class TeleportDecoyListener(IServiceProvider provider) : IPluginModule {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDecoyDetonate(EventDecoyDetonate ev, GameEventInfo _) {
if (ev.Userid == null) return HookResult.Continue;
var player = converter.GetPlayer(ev.Userid) as IOnlinePlayer;
if (player == null) return HookResult.Continue;
if (!shop.HasItem<TeleportDecoyItem>(player)) return HookResult.Continue;
shop.RemoveItem<TeleportDecoyItem>(player);
var vec = new Vector(ev.X, ev.Y, ev.Z + 16);
ev.Userid.Pawn.Value?.Teleport(vec);
return HookResult.Continue;
}
}

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.TeleportDecoy;
public class TeleportDecoyMsgs {
public static IMsg SHOP_ITEM_TELEPORT_DECOY
=> MsgFactory.Create(nameof(SHOP_ITEM_TELEPORT_DECOY));
public static IMsg SHOP_ITEM_TELEPORT_DECOY_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_TELEPORT_DECOY_DESC));
}

View File

@@ -1,8 +1,10 @@
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.Events.Game;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
@@ -17,8 +19,6 @@ public class MapHookListener(IServiceProvider provider)
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
Messenger.DebugAnnounce("Setting role");
var player = converter.GetPlayer(ev.Player);
if (player == null) return;
@@ -34,4 +34,15 @@ public class MapHookListener(IServiceProvider provider)
break;
}
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameInitEvent ev) {
foreach (var player in Utilities.GetPlayers()) {
if (player.Pawn.Value == null) continue;
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "TRAITOR");
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "DETECTIVE");
player.Pawn.Value.AcceptInput("RemoveContext", null, null, "INNOCENT");
}
}
}

View File

@@ -20,8 +20,14 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
private readonly ISet<int> revealedDeaths = new HashSet<int>();
private readonly IDictionary<int, (int, int)> roundKillsAndAssists =
new Dictionary<int, (int, int)>();
private readonly IDictionary<int, RoundData> roundStats =
new Dictionary<int, RoundData>();
record RoundData {
public int Kills;
public int Assists;
public int Damage;
}
public void Dispose() { }
@@ -50,24 +56,39 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
ev.Assister == null ? null : converter.GetPlayer(ev.Assister);
if (killer != null) {
roundKillsAndAssists.TryGetValue(killer.Slot, out var def);
def.Item1++;
roundKillsAndAssists[killer.Slot] = def;
roundStats.TryGetValue(killer.Slot, out var def);
def ??= new RoundData();
def.Kills++;
roundStats[killer.Slot] = def;
}
if (assister != null && assister != killer) {
roundKillsAndAssists.TryGetValue(assister.Slot, out var def);
def.Item2++;
roundKillsAndAssists[assister.Slot] = def;
roundStats.TryGetValue(assister.Slot, out var def);
def ??= new RoundData();
def.Assists++;
roundStats[assister.Slot] = def;
}
}
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnDamage(PlayerDamagedEvent ev) {
var attacker =
ev.Attacker == null ? null : converter.GetPlayer(ev.Attacker);
if (attacker == null) return;
roundStats.TryGetValue(attacker.Slot, out var def);
def ??= new RoundData();
def.Damage += ev.DmgDealt;
roundStats[attacker.Slot] = def;
}
[UsedImplicitly]
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState == State.IN_PROGRESS) {
revealedDeaths.Clear();
roundKillsAndAssists.Clear();
roundStats.Clear();
return;
}
@@ -100,15 +121,16 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
var online = finder.GetOnline()
.Select(p => converter.GetPlayer(p))
.OfType<CCSPlayerController>()
.Where(p => p.IsValid && roundKillsAndAssists.ContainsKey(p.Slot));
.Where(p => p.IsValid && roundStats.ContainsKey(p.Slot));
foreach (var player in online) {
var stats = player.ActionTrackingServices?.MatchStats;
if (stats == null) continue;
var (kills, assists) = roundKillsAndAssists[player.Slot];
stats.Kills += kills;
stats.Assists += assists;
if (!roundStats.TryGetValue(player.Slot, out var data)) continue;
stats.Kills += data.Kills;
stats.Assists += data.Assists;
Utilities.SetStateChanged(player, "CCSPlayerController",
"m_pActionTrackingServices");
}

View File

@@ -114,14 +114,7 @@ public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
// Goal: Pad the name to a fixed width for better alignment in logs
// Left-align ID, right-align name
private string createPaddedName() {
var onlineLengths = Utilities.GetPlayers()
.Select(p => p.PlayerName.Length)
.ToList();
if (onlineLengths.Count == 0) return CreatePaddedName(Id, Name, 13);
var namePadding = Math.Min(onlineLengths.Max(), 24);
return CreatePaddedName(Id, Name, namePadding + 8);
}
private string createPaddedName() { return CreatePaddedName(Id, Name, 24); }
public static string CreatePaddedName(string id, string name, int len) {
var suffix = id.Length > 5 ? id[^5..] : id.PadLeft(5, '0');

View File

@@ -1,29 +0,0 @@
using System.Runtime.InteropServices;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
namespace TTT.CS2.Utils;
public class EntityNameHelper {
private static readonly CEntityIdentity_SetEntityName
CEntityIdentity_SetEntityNameFunc;
// private static MemoryFunctionVoid<CEntityIdentity, string>
// CEntityIdentity_SetEntityNameFunc;
static EntityNameHelper() {
var setEntityNameSignature = NativeAPI.FindSignature(Addresses.ServerPath,
GameData.GetSignature("CEntityIdentity_SetEntityNameFunc"));
CEntityIdentity_SetEntityNameFunc =
Marshal.GetDelegateForFunctionPointer<CEntityIdentity_SetEntityName>(
setEntityNameSignature);
}
private delegate void CEntityIdentity_SetEntityName(IntPtr ptr, string name);
public static void SetEntityName(CCSPlayerController player, string name) {
if (player.Pawn.Value == null) return;
if (player.Pawn.Value.Entity != null)
CEntityIdentity_SetEntityNameFunc(player.Pawn.Value.Entity.Handle, name);
}
}

View File

@@ -33,12 +33,5 @@
"windows": "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 40 48 8B 6C 24 70",
"linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7"
}
},
"CEntityIdentity_SetEntityNameFunc": {
"signatures": {
"library": "server",
"windows": "48 89 5C 24 10 57 48 83 EC 20 48 8B D9 4C 8B C2",
"linux": "55 48 89 F2 48 89 E5 41 55 41 54 53"
}
}
}

View File

@@ -49,4 +49,7 @@ SHOP_ITEM_SILENT_AWP: "Silent AWP"
SHOP_ITEM_SILENT_AWP_DESC: "Receive a silenced AWP with limited ammo."
SHOP_ITEM_CLUSTER_GRENADE: "Cluster Grenade"
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."
SHOP_ITEM_TELEPORT_DECOY: "Teleport Decoy"
SHOP_ITEM_TELEPORT_DECOY_DESC: "A decoy that teleports you to it upon explosion."

View File

@@ -3,6 +3,7 @@ using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.Game.Actions;
@@ -24,6 +25,12 @@ public class DamagedAction(IRoleAssigner roles, IPlayer victim,
public string Verb => "damaged";
public string Details => $"for {Damage} damage with {Weapon}";
public string Prefix
=> PlayerRole != null && OtherRole != null
&& PlayerRole is TraitorRole != OtherRole is TraitorRole ?
"" :
"[BAD] ";
#region ConstructorAliases
public DamagedAction(IServiceProvider provider, IPlayer victim,

View File

@@ -3,6 +3,7 @@ using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.Game.Actions;
@@ -10,7 +11,7 @@ public class DeathAction(IRoleAssigner roles, IPlayer victim, IPlayer? killer)
: IAction {
public DeathAction(IRoleAssigner roles, PlayerDeathEvent ev) : this(roles,
ev.Player, ev.Killer) {
Details = $"using {ev.Weapon}";
if (!string.IsNullOrWhiteSpace(ev.Weapon)) Details = $"using {ev.Weapon}";
}
public IPlayer Player { get; } = victim;
@@ -34,10 +35,16 @@ public class DeathAction(IRoleAssigner roles, IPlayer victim, IPlayer? killer)
$" [{OtherRole.Name.First(char.IsAsciiLetter)}]" :
"";
return Other is not null ?
$"{Other}{oRole} {Verb} {Player}{pRole} {Details}" :
$"{Player}{pRole} {Verb} {Details}";
$"{Prefix}{Other}{oRole} {Verb} {Player}{pRole} {Details}" :
$"{Prefix}{Player}{pRole} {Verb} {Details}";
}
public string Prefix
=> PlayerRole != null && OtherRole != null
&& PlayerRole is TraitorRole != OtherRole is TraitorRole ?
"" :
"[BAD] ";
#region ConstructorAliases
public DeathAction(IServiceProvider provider, IPlayer victim, IPlayer? killer)

View File

@@ -11,5 +11,7 @@ public class RoleAssignedAction(IPlayer player, IRole role) : IAction {
public IRole? OtherRole { get; } = null;
public string Id => "basegame.action.roleassigned";
public string Verb => "was assigned";
public string Details { get; } = role.Name;
public string Details { get; } =
new(role.Name.Where(char.IsAsciiLetter).ToArray());
}

View File

@@ -35,14 +35,18 @@ public sealed class KarmaStorage(IServiceProvider provider) : IKarmaService {
return karmaElement.GetInt32();
}
public Task Write(IPlayer key, int newData) {
var data = new { steam_id = key.Id, karma = newData };
public async Task Write(IPlayer key, int newData) {
var oldKarma = await Load(key);
var karmaUpdateEvent = new KarmaUpdateEvent(key, oldKarma, newData);
provider.GetService<IEventBus>()?.Dispatch(karmaUpdateEvent);
if (karmaUpdateEvent.IsCanceled) return;
var data = new { steam_id = key.Id, karma = karmaUpdateEvent.Karma };
var payload = new StringContent(
System.Text.Json.JsonSerializer.Serialize(data),
System.Text.Encoding.UTF8, "application/json");
return client.PatchAsync("user/" + key.Id, payload);
await client.PatchAsync("user/" + key.Id, payload);
}
public void Dispose() { }

View File

@@ -0,0 +1,16 @@
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
namespace TTT.RTD.Actions;
public class RolledAction(IRoleAssigner roles, IPlayer player, string roll)
: IAction {
public IPlayer Player { get; } = player;
public IPlayer? Other { get; } = null;
public IRole? PlayerRole { get; } = roles.GetRoles(player).FirstOrDefault();
public IRole? OtherRole { get; } = null;
public string Id { get; } = "rtd.action.rolled";
public string Verb { get; } = "rolled";
public string Details { get; } = roll;
}

View File

@@ -8,10 +8,11 @@ public class ShopItemReward<TItem>(IServiceProvider provider)
: RoundStartReward(provider) where TItem : IShopItem {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public override string Name => typeof(TItem).Name;
public override string Name
=> shop.Items.OfType<TItem>().FirstOrDefault()?.Name ?? typeof(TItem).Name;
public override string Description
=> $"you will receive {("aeiou".Contains(Name.ToLower()[0]) ? "an" : "a")} {typeof(TItem).Name} next round";
=> $"you will receive {("aeiou".Contains(Name.ToLower()[0]) ? "an" : "a")} {typeof(TItem).Name} item next round";
public override void GiveOnRound(IOnlinePlayer player) {
var instance = shop.Items.OfType<TItem>().FirstOrDefault();

View File

@@ -4,8 +4,10 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.Events.Game;
using TTT.Locale;
using TTT.RTD.Actions;
namespace TTT.RTD;
@@ -16,12 +18,15 @@ public abstract class RoundStartReward(IServiceProvider provider)
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public abstract string Name { get; }
public abstract string Description { get; }
public void GrantReward(IOnlinePlayer player) { givenPlayers.Add(player); }
public abstract void GiveOnRound(IOnlinePlayer player);
protected readonly IMsgLocalizer Locale =
provider.GetRequiredService<IMsgLocalizer>();
@@ -30,7 +35,10 @@ public abstract class RoundStartReward(IServiceProvider provider)
public virtual void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
foreach (var player in givenPlayers) GiveOnRound(player);
foreach (var player in givenPlayers) {
GiveOnRound(player);
ev.Game.Logger.LogAction(new RolledAction(roles, player, Name));
}
givenPlayers.Clear();
}

View File

@@ -13,7 +13,7 @@ using TTT.Game.Roles;
namespace TTT.Shop.Items.Healthshot;
public static class HealthshotServiceCollection {
public static void AddHealthshot(this IServiceCollection services) {
public static void AddHealthshotServices(this IServiceCollection services) {
services.AddModBehavior<HealthshotItem>();
}
}

View File

@@ -9,7 +9,7 @@ using TTT.Game.Roles;
namespace TTT.Shop.Items.Taser;
public static class TaserServiceCollection {
public static void AddTaserItem(this IServiceCollection collection) {
public static void AddTaserServices(this IServiceCollection collection) {
collection.AddModBehavior<TaserItem>();
}
}

View File

@@ -12,6 +12,7 @@ using TTT.CS2.Items.PoisonShots;
using TTT.CS2.Items.PoisonSmoke;
using TTT.CS2.Items.SilentAWP;
using TTT.CS2.Items.Station;
using TTT.CS2.Items.TeleportDecoy;
using TTT.Shop.Commands;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
@@ -49,15 +50,16 @@ public static class ShopServiceCollection {
collection.AddDeagleServices();
collection.AddDnaScannerServices();
collection.AddGlovesServices();
collection.AddHealthStation();
collection.AddHealthshot();
collection.AddHealthStationServices();
collection.AddHealthshotServices();
collection.AddInnoCompassServices();
collection.AddM4A1Services();
collection.AddOneHitKnifeService();
collection.AddPoisonShots();
collection.AddPoisonSmoke();
collection.AddPoisonShotsServices();
collection.AddPoisonSmokeServices();
collection.AddSilentAWPServices();
collection.AddStickerServices();
collection.AddTaserItem();
collection.AddTaserServices();
collection.AddTeleportDecoyServices();
}
}

View File

@@ -34,7 +34,7 @@ SHOP_CANNOT_PURCHASE_WITH_REASON: "%SHOP_PREFIX%You cannot purchase this item: {
SHOP_PURCHASED: "%SHOP_PREFIX%You purchased {white}{0}{grey}."
SHOP_LIST_FOOTER: "%SHOP_PREFIX%You are %an% {0}{grey}, you have {yellow}{1}{grey} %CREDITS_NAME%%s%."
CREDITS_NAME: "credit"
CREDITS_NAME: "point"
CREDITS_GIVEN: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s%"
CREDITS_GIVEN_REASON: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"

View File

@@ -4,8 +4,8 @@ namespace ShopAPI.Configs.Traitor;
public record SilentAWPConfig : ShopItemConfig, IWeapon {
public override int Price { get; init; } = 80;
public int WeaponIndex { get; } = 9;
public string WeaponId { get; } = "weapon_awp";
public int? ReserveAmmo { get; } = 0;
public int? CurrentAmmo { get; } = 1;
public int WeaponIndex { get; init; } = 9;
public string WeaponId { get; init; } = "weapon_awp";
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;
}

View File

@@ -0,0 +1,5 @@
namespace ShopAPI.Configs.Traitor;
public record TeleportDecoyConfig : ShopItemConfig {
public override int Price { get; init; } = 80;
}

View File

@@ -4,6 +4,7 @@ using ShopAPI.Events;
using SpecialRound.lang;
using SpecialRoundAPI;
using TTT.API.Events;
using TTT.API.Messages;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Locale;
@@ -15,6 +16,12 @@ public class VanillaRound(IServiceProvider provider)
public override string Name => "Vanilla";
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_VANILLA;
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private VanillaRoundConfig config
=> Provider.GetService<IStorage<VanillaRoundConfig>>()
?.Load()
@@ -32,5 +39,7 @@ public class VanillaRound(IServiceProvider provider)
public void OnPurchase(PlayerPurchaseItemEvent ev) {
if (Tracker.CurrentRound != this) return;
ev.IsCanceled = true;
messenger.Message(ev.Player, locale[RoundMsgs.VANILLA_ROUND_REMINDER]);
}
}

View File

@@ -52,10 +52,19 @@ public class SpecialRoundStarter(IServiceProvider provider)
}
private AbstractSpecialRound getSpecialRound() {
return Provider.GetServices<ITerrorModule>()
var rounds = Provider.GetServices<ITerrorModule>()
.OfType<AbstractSpecialRound>()
.OrderBy(_ => Random.Shared.Next())
.First();
.Where(r => r.Config.Weight > 0)
.ToList();
var totalWeight = rounds.Sum(r => r.Config.Weight);
var roll = Random.Shared.NextDouble() * totalWeight;
foreach (var round in rounds) {
roll -= round.Config.Weight;
if (roll <= 0) return round;
}
throw new InvalidOperationException(
"Failed to select a special round. This should never happen.");
}
public AbstractSpecialRound?

View File

@@ -13,6 +13,10 @@ public class RoundMsgs {
public static IMsg SPECIAL_ROUND_BHOP
=> MsgFactory.Create(nameof(SPECIAL_ROUND_BHOP));
public static IMsg SPECIAL_ROUND_VANILLA
=> MsgFactory.Create(nameof(SPECIAL_ROUND_VANILLA)); }
=> MsgFactory.Create(nameof(SPECIAL_ROUND_VANILLA));
public static IMsg VANILLA_ROUND_REMINDER
=> MsgFactory.Create(nameof(VANILLA_ROUND_REMINDER));
}

View File

@@ -1,4 +1,5 @@
SPECIAL_ROUND_STARTED: "%PREFIX%This round is a {purple}Special Round{grey}! This round is a {lightpurple}{0}{grey} round!"
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_VANILLA: " {green}VANILLA{grey}: The shop has been disabled!"
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."