Compare commits

...

12 Commits

Author SHA1 Message Date
MSWS
3b4bf490bc fix: Negative damage logging, server crashes 2025-11-03 21:26:56 -08:00
MSWS
abe75d0347 fix: Adjust detective role color 2025-11-03 21:13:01 -08:00
MSWS
eb79552ba3 fix: Fix speed round config 2025-11-03 21:11:35 -08:00
MSWS
e2011b8d24 feat: Add pistol rounds (resolves #169) 2025-11-03 21:05:43 -08:00
MSWS
ec41a6f367 feat: Add tripwire item (resolves #165) +semver:minor 2025-11-03 20:51:53 -08:00
MSWS
9b1bed6982 Begin work on tripwire item 2025-11-03 16:53:39 -08:00
MSWS
8584877739 feat: Replicate death events to fellow traitors (resolves #171) +semver:minor 2025-11-03 15:30:44 -08:00
MSWS
410dd407b3 update: Handle tag colors as well 2025-11-02 21:23:19 -08:00
MSWS
a0bba2c4ba Add warden tag 2025-11-02 21:16:40 -08:00
MSWS
8aa508bf6d feat: Add victim message to 1-hit weapon 2025-11-02 20:45:23 -08:00
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
26 changed files with 455 additions and 24 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -72,20 +72,19 @@ public static class CS2ServiceCollection {
// 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 +94,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>();

View File

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

View File

@@ -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;
@@ -24,6 +26,9 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
private readonly IAliveSpoofer spoofer =
provider.GetRequiredService<IAliveSpoofer>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public void Start() { }
public void Dispose() { }
@@ -45,7 +50,19 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
return HookResult.Continue;
if (ev.Attacker != null) ev.FireEventToClient(ev.Attacker);
if (ev.Attacker != null) {
ev.FireEventToClient(ev.Attacker);
var apiPlayer = converter.GetPlayer(ev.Attacker);
var role = roles.GetRoles(apiPlayer);
if (role.Any(r => r is TraitorRole))
foreach (var p in Utilities.GetPlayers()) {
var apiP = converter.GetPlayer(p);
if (apiP.Id == apiPlayer.Id) continue;
var r = roles.GetRoles(converter.GetPlayer(p));
if (role.Intersect(r).Any()) ev.FireEventToClient(p);
}
}
info.DontBroadcast = true;
spoofer.SpoofAlive(player);
Server.NextWorldUpdateAsync(() => bus.Dispatch(deathEvent));

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
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));
}

View File

@@ -0,0 +1,55 @@
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.Player;
using TTT.CS2.ThirdParties.eGO;
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 Dictionary<string, (string, char)> oldTags =
new Dictionary<string, (string, char)>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
[UsedImplicitly]
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
var maul = EgoApi.MAUL.Get();
if (maul == null) return;
Server.NextWorldUpdate(() => {
var gamePlayer = converter.GetPlayer(ev.Player);
if (gamePlayer == null) return;
Task.Run(async () => {
if (ev.Role is DetectiveRole) {
var oldTag = await maul.getTagService().GetTag(gamePlayer.SteamID);
var oldTagColor =
await maul.getTagService().GetTagColor(gamePlayer.SteamID);
oldTags[ev.Player.Id] = (oldTag, oldTagColor);
}
await Server.NextWorldUpdateAsync(() => {
if (ev.Role is DetectiveRole) {
maul.getTagService().SetTag(gamePlayer, "[DETECTIVE]", false);
maul.getTagService()
.SetTagColor(gamePlayer, ChatColors.DarkBlue, false);
} else if (oldTags.TryGetValue(ev.Player.Id, out var oldTag)) {
maul.getTagService().SetTag(gamePlayer, oldTag.Item1, false);
maul.getTagService().SetTagColor(gamePlayer, oldTag.Item2, false);
oldTags.Remove(ev.Player.Id);
}
});
});
});
}
}

View File

@@ -52,4 +52,7 @@ SHOP_ITEM_CLUSTER_GRENADE: "Cluster Grenade"
SHOP_ITEM_CLUSTER_GRENADE_DESC: "A grenade that splits into multiple smaller grenades."
SHOP_ITEM_TELEPORT_DECOY: "Teleport Decoy"
SHOP_ITEM_TELEPORT_DECOY_DESC: "A decoy that teleports you to it upon explosion."
SHOP_ITEM_TELEPORT_DECOY_DESC: "A decoy that teleports you to it upon explosion."
SHOP_ITEM_TRIPWIRE: "Tripwire"
SHOP_ITEM_TRIPWIRE_DESC: "A tripwire that explodes when triggered."

View File

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

View File

@@ -54,5 +54,6 @@ public class DeagleDamageListener(IServiceProvider provider)
}
ev.HpLeft = -100;
Messenger.Message(victim, Locale[DeagleMsgs.SHOP_ITEM_DEAGLE_VICTIM]);
}
}

View File

@@ -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));
}

View File

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

View File

@@ -5,6 +5,7 @@ SHOP_ITEM_NOT_FOUND: "%SHOP_PREFIX%Could not find an item named \"{default}{0}{g
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
SHOP_ITEM_DEAGLE_DESC: "A one-hit kill revolver with a single bullet. Aim carefully!"
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."

View File

@@ -0,0 +1,6 @@
namespace ShopAPI.Configs.Traitor;
public record TripwireConfig : ShopItemConfig {
public override int Price { get; init; } = 60;
public int ExplosionPower { get; init; } = 100;
}

View File

@@ -40,7 +40,12 @@ public enum PurchaseResult {
/// <summary>
/// The item cannot be purchased multiple times, and the player already owns it.
/// </summary>
ALREADY_OWNED
ALREADY_OWNED,
/// <summary>
/// The tripwire placement is too far from the player.
/// </summary>
TRIPWIRE_TOO_FAR
}
public static class PurchaseResultExtensions {
@@ -58,7 +63,9 @@ 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",
PurchaseResult.TRIPWIRE_TOO_FAR =>
"The tripwire placement is too far from you",
_ => "An unexpected error occurred"
};
}

View File

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

View File

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

View File

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

View File

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