mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-08 15:27:32 -08:00
Compare commits
20 Commits
0.13.0-dev
...
0.13.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84230fd231 | ||
|
|
fdfc0cc3bd | ||
|
|
7fc0f21fa4 | ||
|
|
ed7ad35b85 | ||
|
|
e6009dd75a | ||
|
|
b295fc45a2 | ||
|
|
385f87ad12 | ||
|
|
6aedbeb3fb | ||
|
|
2d078e4dfa | ||
|
|
ff3dd9563e | ||
|
|
8126dfea21 | ||
|
|
e158bbbd77 | ||
|
|
e382302911 | ||
|
|
75690ee64b | ||
|
|
dadd7b31a1 | ||
|
|
697c7f5d6b | ||
|
|
559718621f | ||
|
|
d84e581392 | ||
|
|
640924d2a2 | ||
|
|
d6da16e537 |
@@ -8,6 +8,7 @@ public interface ICommand : ITerrorModule {
|
||||
string[] RequiredFlags => [];
|
||||
string[] RequiredGroups => [];
|
||||
string[] Aliases => [Id];
|
||||
bool MustBeOnMainThread => false;
|
||||
|
||||
Task<CommandResult> Execute(IOnlinePlayer? executor, ICommandInfo info);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace TTT.API.Player;
|
||||
|
||||
public interface IPlayer {
|
||||
public interface IPlayer : IEquatable<IPlayer> {
|
||||
/// <summary>
|
||||
/// The unique identifier for the player, should
|
||||
/// be unique across all players at all times.
|
||||
@@ -8,4 +8,9 @@ public interface IPlayer {
|
||||
string Id { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
bool IEquatable<IPlayer>.Equals(IPlayer? other) {
|
||||
if (other is null) return false;
|
||||
return Id == other.Id;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<ICommandManager, CS2CommandManager>();
|
||||
collection.AddModBehavior<IAliveSpoofer, CS2AliveSpoofer>();
|
||||
collection.AddModBehavior<IIconManager, RoleIconsHandler>();
|
||||
collection.AddModBehavior<NameDisplayer>();
|
||||
|
||||
// Configs
|
||||
collection.AddModBehavior<IStorage<TTTConfig>, CS2GameConfig>();
|
||||
@@ -43,6 +44,8 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
|
||||
collection.AddModBehavior<IStorage<M4A1Config>, CS2M4A1Config>();
|
||||
collection.AddModBehavior<IStorage<TaserConfig>, CS2TaserConfig>();
|
||||
collection
|
||||
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
|
||||
|
||||
// TTT - CS2 Specific optionals
|
||||
collection.AddScoped<ITextSpawner, TextSpawner>();
|
||||
@@ -53,8 +56,8 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<DamageCanceler>();
|
||||
collection.AddModBehavior<PlayerConnectionsHandler>();
|
||||
collection.AddModBehavior<PropMover>();
|
||||
// collection.AddModBehavior<RoundEnd_GameEndHandler>();
|
||||
collection.AddModBehavior<RoundStart_GameStartHandler>();
|
||||
collection.AddModBehavior<BombPlantSuppressor>();
|
||||
|
||||
// Damage Cancelers
|
||||
collection.AddModBehavior<OutOfRoundCanceler>();
|
||||
|
||||
@@ -40,20 +40,44 @@ public class CS2CommandManager(IServiceProvider provider)
|
||||
var wrapper = executor == null ?
|
||||
null :
|
||||
converter.GetPlayer(executor) as IOnlinePlayer;
|
||||
Task.Run(async () => {
|
||||
try {
|
||||
Console.WriteLine($"Processing command: {cs2Info.CommandString}");
|
||||
return await ProcessCommand(cs2Info);
|
||||
} catch (Exception e) {
|
||||
var msg = e.Message;
|
||||
cs2Info.ReplySync(Localizer[GameMsgs.GENERIC_ERROR(msg)]);
|
||||
await Server.NextWorldUpdateAsync(() => {
|
||||
|
||||
if (cmdMap.TryGetValue(info.GetArg(0), out var command))
|
||||
if (command.MustBeOnMainThread) {
|
||||
processCommandSync(cs2Info, wrapper);
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(async () => await processCommandAsync(cs2Info, wrapper));
|
||||
}
|
||||
|
||||
private async Task<CommandResult> processCommandAsync(CS2CommandInfo cs2Info,
|
||||
IOnlinePlayer? wrapper) {
|
||||
try {
|
||||
Console.WriteLine($"Processing command: {cs2Info.CommandString}");
|
||||
return await ProcessCommand(cs2Info);
|
||||
} catch (Exception e) {
|
||||
var msg = e.Message;
|
||||
cs2Info.ReplySync(Localizer[GameMsgs.GENERIC_ERROR(msg)]);
|
||||
await Server.NextWorldUpdateAsync(() => {
|
||||
Console.WriteLine(
|
||||
$"Encountered an error when processing command: \"{cs2Info.CommandString}\" by {wrapper?.Id}");
|
||||
Console.WriteLine(e);
|
||||
});
|
||||
return CommandResult.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
private void processCommandSync(CS2CommandInfo cs2Info,
|
||||
IOnlinePlayer? wrapper) {
|
||||
try { _ = ProcessCommand(cs2Info); } catch (Exception e) {
|
||||
var msg = e.Message;
|
||||
cs2Info.ReplySync(Localizer[GameMsgs.GENERIC_ERROR(msg)]);
|
||||
Server.NextWorldUpdateAsync(() => {
|
||||
Console.WriteLine(
|
||||
$"Encountered an error when processing command: \"{cs2Info.CommandString}\" by {wrapper?.Id}");
|
||||
Console.WriteLine(e);
|
||||
});
|
||||
return CommandResult.ERROR;
|
||||
}
|
||||
});
|
||||
})
|
||||
.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ using TTT.API.Player;
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace TTT.CS2.Configs.ShopItems;
|
||||
|
||||
public class CS2C4Config : IStorage<C4Config>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_PRICE = new("css_ttt_shop_c4_price",
|
||||
"Price of the C4 item", 140, ConVarFlags.FCVAR_NONE,
|
||||
"Price of the C4 item", 130, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_WEAPON = new(
|
||||
|
||||
@@ -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", 120,
|
||||
"css_ttt_shop_onedeagle_price", "Price of the One-Shot Deagle item", 100,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
|
||||
|
||||
67
TTT/CS2/Configs/ShopItems/CS2PoisonSmokeConfig.cs
Normal file
67
TTT/CS2/Configs/ShopItems/CS2PoisonSmokeConfig.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
using TTT.CS2.Validators;
|
||||
|
||||
namespace TTT.CS2.Configs.ShopItems;
|
||||
|
||||
public class CS2PoisonSmokeConfig : IStorage<PoisonSmokeConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_PRICE = new(
|
||||
"css_ttt_shop_poisonsmoke_price", "Price of the Poison Smoke item", 45,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_WEAPON = new(
|
||||
"css_ttt_shop_poisonsmoke_weapon",
|
||||
"Weapon entity name used for the Poison Smoke item", "weapon_smokegrenade",
|
||||
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
|
||||
|
||||
// Poison effect sub-config
|
||||
public static readonly FakeConVar<int> CV_POISON_TICK_DAMAGE = new(
|
||||
"css_ttt_shop_poisonsmoke_poison_damage_per_tick",
|
||||
"Damage dealt per poison tick", 15, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(1, 100));
|
||||
|
||||
public static readonly FakeConVar<int> CV_POISON_TOTAL_DAMAGE = new(
|
||||
"css_ttt_shop_poisonsmoke_poison_total_damage",
|
||||
"Total damage dealt over the poison duration", 500, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(1, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_POISON_TICK_INTERVAL = new(
|
||||
"css_ttt_shop_poisonsmoke_poison_tick_interval",
|
||||
"Milliseconds between each poison damage tick", 500, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(100, 10000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_POISON_SOUND = new(
|
||||
"css_ttt_shop_poisonsmoke_poison_sound",
|
||||
"Sound played when poison deals damage",
|
||||
"sounds/player/player_damagebody_03");
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
|
||||
plugin.RegisterFakeConVars(this);
|
||||
}
|
||||
|
||||
public Task<PoisonSmokeConfig?> Load() {
|
||||
var poison = new PoisonConfig {
|
||||
TimeBetweenDamage =
|
||||
TimeSpan.FromMilliseconds(CV_POISON_TICK_INTERVAL.Value),
|
||||
DamagePerTick = CV_POISON_TICK_DAMAGE.Value,
|
||||
TotalDamage = CV_POISON_TOTAL_DAMAGE.Value,
|
||||
PoisonSound = CV_POISON_SOUND.Value
|
||||
};
|
||||
|
||||
var cfg = new PoisonSmokeConfig {
|
||||
Price = CV_PRICE.Value, Weapon = CV_WEAPON.Value, PoisonConfig = poison
|
||||
};
|
||||
|
||||
return Task.FromResult<PoisonSmokeConfig?>(cfg);
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,9 @@ public static class PlayerExtensions {
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
pawn.ArmorValue = armor;
|
||||
if (withHelmet) {
|
||||
if (withHelmet)
|
||||
if (pawn.ItemServices != null)
|
||||
new CCSPlayer_ItemServices(pawn.ItemServices.Handle).HasHelmet = true;
|
||||
}
|
||||
|
||||
Utilities.SetStateChanged(pawn, "CCSPlayerPawn", "m_ArmorValue");
|
||||
}
|
||||
|
||||
20
TTT/CS2/GameHandlers/BombPlantSuppressor.cs
Normal file
20
TTT/CS2/GameHandlers/BombPlantSuppressor.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using JetBrains.Annotations;
|
||||
using TTT.API;
|
||||
|
||||
namespace TTT.CS2.GameHandlers;
|
||||
|
||||
public class BombPlantSuppressor : IPluginModule {
|
||||
public void Dispose() { }
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
plugin?.HookUserMessage(322, um => {
|
||||
um.Recipients.Clear();
|
||||
return HookResult.Handled;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
|
||||
}
|
||||
|
||||
var killerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
|
||||
ev.Attacker.ActionTrackingServices.NumRoundKills--;
|
||||
Utilities.SetStateChanged(ev.Attacker,
|
||||
"CCSPlayerController_ActionTrackingServices",
|
||||
"m_pActionTrackingServices");
|
||||
if (killerStats == null) return;
|
||||
killerStats.Kills -= 1;
|
||||
killerStats.Damage -= ev.DmgHealth;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using TTT.API.Events;
|
||||
using JetBrains.Annotations;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.CS2.Utils;
|
||||
using TTT.Game.Events.Player;
|
||||
@@ -8,10 +9,11 @@ namespace TTT.CS2.GameHandlers.DamageCancelers;
|
||||
|
||||
public class OutOfRoundCanceler(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnHurt(PlayerDamagedEvent ev) {
|
||||
if (RoundUtil.IsWarmup()) return;
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS })
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS or State.FINISHED })
|
||||
ev.IsCanceled = true;
|
||||
}
|
||||
}
|
||||
34
TTT/CS2/GameHandlers/NameDisplayer.cs
Normal file
34
TTT/CS2/GameHandlers/NameDisplayer.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using TTT.API;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.CS2.RayTrace.Class;
|
||||
using TTT.CS2.RayTrace.Enum;
|
||||
|
||||
namespace TTT.CS2.GameHandlers;
|
||||
|
||||
public class NameDisplayer : IPluginModule {
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
plugin?.AddTimer(0.25f, showNames, TimerFlags.REPEAT);
|
||||
}
|
||||
|
||||
private void showNames() {
|
||||
foreach (var player in Utilities.GetPlayers()) {
|
||||
if (player.GetHealth() <= 0) continue;
|
||||
|
||||
var target = player.GetGameTraceByEyePosition(TraceMask.MaskSolid,
|
||||
Contents.NoDraw, player);
|
||||
|
||||
if (target == null) continue;
|
||||
|
||||
if (!target.Value.HitPlayer(out var hit)) continue;
|
||||
if (hit == null) continue;
|
||||
|
||||
player.PrintToCenterAlert($"{hit.PlayerName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,12 +187,11 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
|
||||
targetVector.Z = Math.Max(targetVector.Z, playerOrigin.Z - 48);
|
||||
|
||||
if (ent.AbsOrigin == null) return;
|
||||
var lerpedVector = ent.AbsOrigin.Lerp(targetVector, 0.3f);
|
||||
|
||||
if (info.Beam != null && info.Beam.IsValid) {
|
||||
info.Beam.AcceptInput("Kill");
|
||||
info.Beam = createBeam(playerOrigin.With(z: playerOrigin.Z - 16),
|
||||
lerpedVector);
|
||||
ent.AbsOrigin);
|
||||
}
|
||||
|
||||
playersPressingE[player] = info;
|
||||
@@ -201,9 +200,9 @@ public class PropMover(IServiceProvider provider) : IPluginModule {
|
||||
private CEnvBeam? createBeam(Vector start, Vector end) {
|
||||
var beam = Utilities.CreateEntityByName<CEnvBeam>("env_beam");
|
||||
if (beam == null) return null;
|
||||
beam.RenderMode = RenderMode_t.kRenderTransColor;
|
||||
beam.Width = 0.5f;
|
||||
beam.Render = Color.White;
|
||||
beam.RenderMode = RenderMode_t.kRenderTransAlpha;
|
||||
beam.Width = 2.0f;
|
||||
beam.Render = Color.FromArgb(32, Color.White);
|
||||
beam.EndPos.X = end.X;
|
||||
beam.EndPos.Y = end.Y;
|
||||
beam.EndPos.Z = end.Z;
|
||||
|
||||
@@ -77,6 +77,7 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
plugin
|
||||
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.CheckTransmit>(
|
||||
onTransmit);
|
||||
if (hotReload) OnRoundEnd(null!, null!);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
|
||||
@@ -8,7 +8,7 @@ public class TextSetting {
|
||||
public float depthOffset = 0.0f;
|
||||
public bool enabled = true;
|
||||
public string fontName = "Arial";
|
||||
public float fontSize = 50;
|
||||
public float fontSize = 64;
|
||||
public bool fullbright = true;
|
||||
|
||||
public PointWorldTextJustifyHorizontal_t horizontal =
|
||||
@@ -23,5 +23,5 @@ public class TextSetting {
|
||||
public PointWorldTextJustifyVertical_t vertical =
|
||||
PointWorldTextJustifyVertical_t.POINT_WORLD_TEXT_JUSTIFY_VERTICAL_CENTER;
|
||||
|
||||
public float worldUnitsPerPx = 0.4f;
|
||||
public float worldUnitsPerPx = 0.5f;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ public class TextSpawner : ITextSpawner {
|
||||
position.Add(new Vector(0, 0, 72));
|
||||
rotation = new QAngle(rotation.X, rotation.Y + yRot, rotation.Z + 90);
|
||||
|
||||
position.Add(rotation.ToRight() * -10);
|
||||
position.Add(rotation.ToRight() * 5);
|
||||
|
||||
var ent = CreateText(setting, position, rotation);
|
||||
ent.AcceptInput("SetParent", player.Pawn.Value, null, "!activator");
|
||||
|
||||
@@ -22,13 +22,13 @@ public class ArmorItem(IServiceProvider provider) : BaseItem(provider) {
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new ArmorConfig();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
public override string Name => Locale[ArmorMsgs.SHOP_ITEM_ARMOR];
|
||||
public override string Description => Locale[ArmorMsgs.SHOP_ITEM_ARMOR_DESC];
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
var gamePlayer = converter.GetPlayer(player);
|
||||
if (gamePlayer == null) return;
|
||||
|
||||
@@ -2,7 +2,6 @@ using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Events;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
@@ -16,17 +15,18 @@ namespace TTT.CS2.Items.BodyPaint;
|
||||
|
||||
public class BodyPaintListener(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly IBodyTracker bodies =
|
||||
provider.GetRequiredService<IBodyTracker>();
|
||||
|
||||
private readonly BodyPaintConfig config =
|
||||
provider.GetService<IStorage<BodyPaintConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new BodyPaintConfig();
|
||||
|
||||
private readonly Dictionary<IPlayer, int> uses = new();
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IBodyTracker bodies =
|
||||
provider.GetRequiredService<IBodyTracker>();
|
||||
private readonly Dictionary<IPlayer, int> uses = new();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.HIGH)]
|
||||
|
||||
@@ -8,7 +8,7 @@ public class BodyPaintMsgs {
|
||||
|
||||
public static IMsg SHOP_ITEM_BODY_PAINT_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_DESC));
|
||||
|
||||
|
||||
public static IMsg SHOP_ITEM_BODY_PAINT_OUT
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_OUT));
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Detective;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
|
||||
@@ -10,9 +10,10 @@ public class PoisonShotMsgs {
|
||||
public static IMsg SHOP_ITEM_POISON_SHOTS_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SHOTS_DESC));
|
||||
|
||||
public static IMsg SHOP_ITEM_POISON_HIT(IPlayer player)
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_HIT), player.Name);
|
||||
|
||||
public static IMsg SHOP_ITEM_POISON_OUT
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_OUT));
|
||||
|
||||
public static IMsg SHOP_ITEM_POISON_HIT(IPlayer player) {
|
||||
return MsgFactory.Create(nameof(SHOP_ITEM_POISON_HIT), player.Name);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
using System.Drawing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive.Concurrency;
|
||||
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 ShopAPI.Configs.Traitor;
|
||||
using ShopAPI.Events;
|
||||
using TTT.API;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
@@ -23,8 +21,6 @@ namespace TTT.CS2.Items.PoisonShots;
|
||||
|
||||
public class PoisonShotsListener(IServiceProvider provider)
|
||||
: BaseListener(provider), IPluginModule {
|
||||
private readonly Dictionary<IPlayer, int> poisonShots = new();
|
||||
|
||||
private readonly PoisonShotsConfig config =
|
||||
provider.GetService<IStorage<PoisonShotsConfig>>()
|
||||
?.Load()
|
||||
@@ -34,13 +30,20 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
private readonly Dictionary<IPlayer, int> poisonShots = new();
|
||||
|
||||
private readonly List<IDisposable> poisonTimers = [];
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
foreach (var timer in poisonTimers) timer.Dispose();
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnFire(EventWeaponFire ev, GameEventInfo _) {
|
||||
@@ -73,16 +76,20 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
|
||||
foreach (var timer in poisonTimers) timer.Dispose();
|
||||
poisonTimers.Clear();
|
||||
poisonShots.Clear();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "AccessToModifiedClosure")]
|
||||
private void addPoisonEffect(IPlayer player) {
|
||||
IDisposable? timer = null;
|
||||
|
||||
var effect = new PoisonEffect(player);
|
||||
timer = scheduler.SchedulePeriodic(config.TimeBetweenDamage, () => {
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
timer = scheduler.SchedulePeriodic(config.PoisonConfig.TimeBetweenDamage, ()
|
||||
=> {
|
||||
Server.NextWorldUpdate(() => {
|
||||
if (!tickPoison(effect)) timer?.Dispose();
|
||||
if (tickPoison(effect) || timer == null) return;
|
||||
timer.Dispose();
|
||||
poisonTimers.Remove(timer);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,31 +99,20 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
private bool tickPoison(PoisonEffect effect) {
|
||||
if (effect.Player is not IOnlinePlayer online) return false;
|
||||
if (!online.IsAlive) return false;
|
||||
online.Health -= config.DamagePerTick;
|
||||
online.Health -= config.PoisonConfig.DamagePerTick;
|
||||
effect.Ticks++;
|
||||
effect.DamageGiven += config.DamagePerTick;
|
||||
effect.DamageGiven += config.PoisonConfig.DamagePerTick;
|
||||
|
||||
var gamePlayer = converter.GetPlayer(online);
|
||||
gamePlayer?.ColorScreen(config.PoisonColor, 0.2f, 0.3f);
|
||||
gamePlayer?.ExecuteClientCommand("play " + config.PoisonSound);
|
||||
gamePlayer?.ExecuteClientCommand("play " + config.PoisonConfig.PoisonSound);
|
||||
|
||||
return effect.DamageGiven < config.TotalDamage;
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
foreach (var timer in poisonTimers) timer.Dispose();
|
||||
}
|
||||
|
||||
private class PoisonEffect(IPlayer player) {
|
||||
public IPlayer Player { get; init; } = player;
|
||||
public int Ticks { get; set; }
|
||||
public int DamageGiven { get; set; }
|
||||
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses a poison shot for the player. Returns the remaining shots, -1 if none
|
||||
/// are available.
|
||||
/// Uses a poison shot for the player. Returns the remaining shots, -1 if none
|
||||
/// are available.
|
||||
/// </summary>
|
||||
/// <param name="player"></param>
|
||||
/// <returns></returns>
|
||||
@@ -133,4 +129,10 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
shop.RemoveItem<PoisonShotsItem>(player);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private class PoisonEffect(IPlayer player) {
|
||||
public IPlayer Player { get; } = player;
|
||||
public int Ticks { get; set; }
|
||||
public int DamageGiven { get; set; }
|
||||
}
|
||||
}
|
||||
43
TTT/CS2/Items/PoisonSmoke/PoisonSmokeItem.cs
Normal file
43
TTT/CS2/Items/PoisonSmoke/PoisonSmokeItem.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
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.PoisonSmoke;
|
||||
|
||||
public static class PoisonSmokeServiceCollection {
|
||||
public static void AddPoisonSmoke(this IServiceCollection services) {
|
||||
services.AddModBehavior<PoisonSmokeItem>();
|
||||
services.AddModBehavior<PoisonSmokeListener>();
|
||||
}
|
||||
}
|
||||
|
||||
public class PoisonSmokeItem(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider) {
|
||||
private readonly PoisonSmokeConfig config =
|
||||
provider.GetService<IStorage<PoisonSmokeConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new PoisonSmokeConfig();
|
||||
|
||||
public override string Name => Locale[PoisonSmokeMsgs.SHOP_ITEM_POISON_SMOKE];
|
||||
|
||||
public override string Description
|
||||
=> Locale[PoisonSmokeMsgs.SHOP_ITEM_POISON_SMOKE_DESC];
|
||||
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
|
||||
}
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
return Shop.HasItem<PoisonSmokeItem>(player) ?
|
||||
PurchaseResult.ALREADY_OWNED :
|
||||
base.CanPurchase(player);
|
||||
}
|
||||
}
|
||||
120
TTT/CS2/Items/PoisonSmoke/PoisonSmokeListener.cs
Normal file
120
TTT/CS2/Items/PoisonSmoke/PoisonSmokeListener.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reactive.Concurrency;
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.PoisonSmoke;
|
||||
|
||||
public class PoisonSmokeListener(IServiceProvider provider) : IPluginModule {
|
||||
private readonly PoisonSmokeConfig config =
|
||||
provider.GetService<IStorage<PoisonSmokeConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new PoisonSmokeConfig();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
private readonly List<IDisposable> poisonSmokes = [];
|
||||
|
||||
private readonly IRoleAssigner roleAssigner =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public void Dispose() {
|
||||
foreach (var timer in poisonSmokes) timer.Dispose();
|
||||
|
||||
poisonSmokes.Clear();
|
||||
}
|
||||
|
||||
public void Start() { }
|
||||
|
||||
[GameEventHandler]
|
||||
public HookResult OnSmokeGrenade(EventSmokegrenadeDetonate 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<PoisonSmokeItem>(player)) return HookResult.Continue;
|
||||
|
||||
shop.RemoveItem<PoisonSmokeItem>(player);
|
||||
|
||||
var projectile =
|
||||
Utilities.GetEntityFromIndex<CSmokeGrenadeProjectile>(ev.Entityid);
|
||||
if (projectile == null || !projectile.IsValid) return HookResult.Continue;
|
||||
startPoisonEffect(projectile);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "AccessToModifiedClosure")]
|
||||
private void startPoisonEffect(CSmokeGrenadeProjectile projectile) {
|
||||
IDisposable? timer = null;
|
||||
|
||||
var effect = new PoisonEffect(projectile);
|
||||
|
||||
timer = scheduler.SchedulePeriodic(config.PoisonConfig.TimeBetweenDamage, ()
|
||||
=> {
|
||||
Server.NextWorldUpdate(() => {
|
||||
if (tickPoisonEffect(effect) || timer == null) return;
|
||||
timer.Dispose();
|
||||
poisonSmokes.Remove(timer);
|
||||
});
|
||||
});
|
||||
|
||||
poisonSmokes.Add(timer);
|
||||
}
|
||||
|
||||
private bool tickPoisonEffect(PoisonEffect effect) {
|
||||
if (!effect.Projectile.IsValid) return false;
|
||||
effect.Ticks++;
|
||||
|
||||
var players = finder.GetOnline()
|
||||
.Where(player => player.IsAlive && roleAssigner.GetRoles(player)
|
||||
.Any(role => role is InnocentRole or DetectiveRole));
|
||||
|
||||
var gamePlayers = players.Select(p => converter.GetPlayer(p))
|
||||
.Where(p => p != null && p.Pawn.Value != null && p.Pawn.Value.IsValid)
|
||||
.Select(p => (p!, p?.Pawn.Value?.AbsOrigin.Clone()!));
|
||||
|
||||
gamePlayers = gamePlayers.Where(t
|
||||
=> t.Item2.Distance(effect.Origin) <= config.SmokeRadius);
|
||||
|
||||
foreach (var player in gamePlayers.Select(p => p.Item1)) {
|
||||
if (effect.DamageGiven >= config.PoisonConfig.TotalDamage) continue;
|
||||
player.AddHealth(-config.PoisonConfig.DamagePerTick);
|
||||
player.ExecuteClientCommand("play " + config.PoisonConfig.PoisonSound);
|
||||
effect.DamageGiven += config.PoisonConfig.DamagePerTick;
|
||||
}
|
||||
|
||||
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
|
||||
}
|
||||
|
||||
private class PoisonEffect(CSmokeGrenadeProjectile projectile) {
|
||||
public int Ticks { get; set; }
|
||||
public int DamageGiven { get; set; }
|
||||
public Vector Origin { get; } = projectile.AbsOrigin.Clone()!;
|
||||
public CSmokeGrenadeProjectile Projectile { get; } = projectile;
|
||||
}
|
||||
}
|
||||
11
TTT/CS2/Items/PoisonSmoke/PoisonSmokeMsgs.cs
Normal file
11
TTT/CS2/Items/PoisonSmoke/PoisonSmokeMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.PoisonSmoke;
|
||||
|
||||
public class PoisonSmokeMsgs {
|
||||
public static IMsg SHOP_ITEM_POISON_SMOKE
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SMOKE));
|
||||
|
||||
public static IMsg SHOP_ITEM_POISON_SMOKE_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_POISON_SMOKE_DESC));
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Player;
|
||||
@@ -17,11 +16,12 @@ public static class DamageStationCollection {
|
||||
}
|
||||
}
|
||||
|
||||
public class DamageStation(IServiceProvider provider) : StationItem(provider,
|
||||
provider.GetService<IStorage<DamageStationConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new DamageStationConfig()) {
|
||||
public class DamageStation(IServiceProvider provider)
|
||||
: StationItem<TraitorRole>(provider,
|
||||
provider.GetService<IStorage<DamageStationConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new DamageStationConfig()) {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
@@ -37,10 +37,12 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
|
||||
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
|
||||
|
||||
override protected void onInterval() {
|
||||
var players = finder.GetOnline();
|
||||
var players = finder.GetOnline();
|
||||
var toRemove = new List<CPhysicsPropMultiplayer>();
|
||||
foreach (var (prop, info) in props) {
|
||||
if (Math.Abs(info.HealthGiven) > Math.Abs(_Config.TotalHealthGiven)) {
|
||||
props.Remove(prop);
|
||||
if (_Config.TotalHealthGiven != 0 && Math.Abs(info.HealthGiven)
|
||||
> Math.Abs(_Config.TotalHealthGiven)) {
|
||||
toRemove.Add(prop);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -71,5 +73,7 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
|
||||
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in toRemove) props.Remove(prop);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Detective;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.Station;
|
||||
|
||||
@@ -14,21 +15,24 @@ public static class HealthStationCollection {
|
||||
}
|
||||
}
|
||||
|
||||
public class HealthStation(IServiceProvider provider) : StationItem(provider,
|
||||
provider.GetService<IStorage<HealthStationConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new HealthStationConfig()) {
|
||||
public class HealthStation(IServiceProvider provider)
|
||||
: StationItem<DetectiveRole>(provider,
|
||||
provider.GetService<IStorage<HealthStationConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new HealthStationConfig()) {
|
||||
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH];
|
||||
|
||||
public override string Description
|
||||
=> Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH_DESC];
|
||||
|
||||
override protected void onInterval() {
|
||||
var players = Utilities.GetPlayers();
|
||||
var players = Utilities.GetPlayers();
|
||||
var toRemove = new List<CPhysicsPropMultiplayer>();
|
||||
foreach (var (prop, info) in props) {
|
||||
if (Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
|
||||
props.Remove(prop);
|
||||
if (_Config.TotalHealthGiven != 0
|
||||
&& Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
|
||||
toRemove.Add(prop);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -54,5 +58,7 @@ public class HealthStation(IServiceProvider provider) : StationItem(provider,
|
||||
player.ExecuteClientCommand("play " + _Config.UseSound);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in toRemove) props.Remove(prop);
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.Station;
|
||||
|
||||
public abstract class StationItem(IServiceProvider provider,
|
||||
public abstract class StationItem<T>(IServiceProvider provider,
|
||||
StationConfig config)
|
||||
: RoleRestrictedItem<DetectiveRole>(provider), IPluginModule {
|
||||
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
|
||||
private static readonly long PROP_SIZE_SQUARED = 500;
|
||||
protected readonly StationConfig _Config = config;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace TTT.CS2.Player;
|
||||
/// Non-human Players (bots) will be tracked by their entity index.
|
||||
/// Note that slot numbers are not guaranteed to be stable across server restarts.
|
||||
/// </summary>
|
||||
public class CS2Player : IOnlinePlayer {
|
||||
public class CS2Player : IOnlinePlayer, IEquatable<CS2Player> {
|
||||
private CCSPlayerController? cachePlayer;
|
||||
|
||||
protected CS2Player(string id, string name) {
|
||||
@@ -51,6 +51,11 @@ public class CS2Player : IOnlinePlayer {
|
||||
=> Math.Min(Utilities.GetPlayers().Select(p => p.PlayerName.Length).Max(),
|
||||
24);
|
||||
|
||||
public bool Equals(CS2Player? other) {
|
||||
if (other is null) return false;
|
||||
return Id == other.Id;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
|
||||
@@ -107,6 +112,8 @@ public class CS2Player : IOnlinePlayer {
|
||||
return player.SteamID.ToString();
|
||||
}
|
||||
|
||||
public override int GetHashCode() { return Id.GetHashCode(); }
|
||||
|
||||
public override string ToString() { return createPaddedName(); }
|
||||
|
||||
// Goal: Pad the name to a fixed width for better alignment in logs
|
||||
|
||||
@@ -25,5 +25,8 @@ SHOP_ITEM_POISON_SHOTS_DESC: "Your bullets are coated in a mildly poisonous subs
|
||||
SHOP_ITEM_POISON_HIT: "%PREFIX% You hit {green}{0}{grey} with a {lightpurple}poison shot{grey}."
|
||||
SHOP_ITEM_POISON_OUT: "%PREFIX% You are out of poison shots."
|
||||
|
||||
SHOP_ITEM_POISON_SMOKE: "Poison Smoke"
|
||||
SHOP_ITEM_POISON_SMOKE_DESC: "Throw a grenade that releases poisonous gas."
|
||||
|
||||
SHOP_ITEM_ARMOR: "Armor with Helmet"
|
||||
SHOP_ITEM_ARMOR_DESC: "Wear armor that reduces incoming damage."
|
||||
@@ -20,6 +20,8 @@ public static class GameServiceCollection {
|
||||
collection.AddModBehavior<PlayerJoinStarting>();
|
||||
collection.AddModBehavior<PlayerActionsLogger>();
|
||||
collection.AddModBehavior<BodyIdentifyLogger>();
|
||||
collection.AddModBehavior<PlayerDeathInformer>();
|
||||
collection.AddModBehavior<TraitorBuddyInformer>();
|
||||
|
||||
// Commands
|
||||
collection.AddModBehavior<TTTCommand>();
|
||||
|
||||
19
TTT/Game/Listeners/PlayerDeathInformer.cs
Normal file
19
TTT/Game/Listeners/PlayerDeathInformer.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using JetBrains.Annotations;
|
||||
using TTT.API.Events;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.lang;
|
||||
|
||||
namespace TTT.Game.Listeners;
|
||||
|
||||
public class PlayerDeathInformer(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnDeath(PlayerDeathEvent ev) {
|
||||
if (ev.Killer == null) return;
|
||||
var killerRole = Roles.GetRoles(ev.Killer).FirstOrDefault();
|
||||
if (killerRole == null) return;
|
||||
Messenger.Message(ev.Victim,
|
||||
Locale[GameMsgs.ROLE_REVEAL_DEATH(killerRole)]);
|
||||
}
|
||||
}
|
||||
33
TTT/Game/Listeners/TraitorBuddyInformer.cs
Normal file
33
TTT/Game/Listeners/TraitorBuddyInformer.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.lang;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.Game.Listeners;
|
||||
|
||||
public class TraitorBuddyInformer(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnGameStatChange(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.IN_PROGRESS) return;
|
||||
|
||||
var traitors = ev.Game.GetAlive(typeof(TraitorRole));
|
||||
|
||||
foreach (var traitor in traitors) {
|
||||
var buddies = traitors.Where(x => x != traitor).ToList();
|
||||
if (buddies.Count == 0) {
|
||||
Messenger.Message(traitor, Locale[GameMsgs.ROLE_REVEAL_TRAITORS_NONE]);
|
||||
} else {
|
||||
Messenger.Message(traitor,
|
||||
Locale[GameMsgs.ROLE_REVEAL_TRAITORS_HEADER]);
|
||||
foreach (var buddy in buddies)
|
||||
Messenger.Message(traitor,
|
||||
$" {ChatColors.Grey}- {ChatColors.Red}{buddy.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,9 +172,6 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
foreach (var player in online) inventory.RemoveAllWeapons(player);
|
||||
|
||||
StartedAt = DateTime.Now;
|
||||
RoleAssigner.AssignRoles(online, Roles);
|
||||
players.AddRange(online);
|
||||
|
||||
@@ -32,7 +32,7 @@ public record TTTConfig {
|
||||
|
||||
public string[]? InnocentWeapons { get; init; } = ["knife", "pistol"];
|
||||
|
||||
public bool StripWeaponsPriorToEquipping { get; init; } = true;
|
||||
public bool StripWeaponsPriorToEquipping { get; init; } = false;
|
||||
}
|
||||
|
||||
public record RoundConfig {
|
||||
|
||||
@@ -15,12 +15,23 @@ public static class GameMsgs {
|
||||
public static IMsg ROLE_DETECTIVE
|
||||
=> MsgFactory.Create(nameof(ROLE_DETECTIVE));
|
||||
|
||||
public static IMsg ROLE_REVEAL_TRAITORS_HEADER
|
||||
=> MsgFactory.Create(nameof(ROLE_REVEAL_TRAITORS_HEADER));
|
||||
|
||||
public static IMsg ROLE_REVEAL_TRAITORS_NONE
|
||||
=> MsgFactory.Create(nameof(ROLE_REVEAL_TRAITORS_NONE));
|
||||
|
||||
public static IMsg GAME_LOGS_HEADER
|
||||
=> MsgFactory.Create(nameof(GAME_LOGS_HEADER));
|
||||
|
||||
public static IMsg GAME_LOGS_FOOTER
|
||||
=> MsgFactory.Create(nameof(GAME_LOGS_FOOTER));
|
||||
|
||||
public static IMsg ROLE_REVEAL_DEATH(IRole killerRole) {
|
||||
return MsgFactory.Create(nameof(ROLE_REVEAL_DEATH),
|
||||
GetRolePrefix(killerRole) + killerRole.Name);
|
||||
}
|
||||
|
||||
public static IMsg ROLE_ASSIGNED(IRole role) {
|
||||
return MsgFactory.Create(nameof(ROLE_ASSIGNED), role.Name);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ ROLE_INNOCENT: "{green}Innocent"
|
||||
ROLE_DETECTIVE: "{blue}Detective"
|
||||
ROLE_TRAITOR: "{red}Traitor"
|
||||
ROLE_ASSIGNED: "%PREFIX%You are %an% {0}{grey}!"
|
||||
ROLE_REVEAL_DEATH: "%PREFIX%Your killer was %an% {0}{grey}!"
|
||||
ROLE_REVEAL_TRAITORS_HEADER: "%PREFIX%Your {red}Traitor {grey}teammates are:"
|
||||
ROLE_REVEAL_TRAITORS_NONE: "%PREFIX%You have no {red}Traitor {grey}teammates."
|
||||
GENERIC_UNKNOWN: "%PREFIX%{red}Unknown Command: {darkred}{0}"
|
||||
GENERIC_NO_PERMISSION: "%PREFIX%{red}You do not have permission to use this command."
|
||||
GENERIC_NO_PERMISSION_NODE: "%PREFIX%{red}You are missing the {darkred}{0}{red} permission."
|
||||
|
||||
@@ -2,11 +2,16 @@
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Commands;
|
||||
|
||||
public class BalanceCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IMsgLocalizer locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public string Id => "balance";
|
||||
public string[] Aliases => [Id, "bal", "credits", "money"];
|
||||
|
||||
@@ -21,7 +26,7 @@ public class BalanceCommand(IServiceProvider provider) : ICommand {
|
||||
}
|
||||
|
||||
var bal = await shop.Load(executor);
|
||||
info.ReplySync($"You have {bal} credits.");
|
||||
info.ReplySync(locale[ShopMsgs.COMMAND_BALANCE(bal)]);
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
public string Id => "buy";
|
||||
public void Start() { }
|
||||
public string[] Aliases => [Id, "purchase", "b"];
|
||||
public string[] Usage => ["[item]"];
|
||||
|
||||
public bool MustBeOnMainThread => true;
|
||||
|
||||
public Task<CommandResult> Execute(IOnlinePlayer? executor,
|
||||
ICommandInfo info) {
|
||||
@@ -58,7 +61,7 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
|
||||
if (item != null) return item;
|
||||
|
||||
item = shop.Items.FirstOrDefault(it
|
||||
=> it.Name.Equals(query, StringComparison.OrdinalIgnoreCase));
|
||||
=> it.Name.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (item != null) return item;
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.Shop.Commands;
|
||||
|
||||
public class ListCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
private readonly IGameManager games = provider
|
||||
.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
@@ -19,12 +19,51 @@ public class ListCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
foreach (var item in shop.Items)
|
||||
messenger.Message(executor,
|
||||
$"{ChatColors.Grey}- {ChatColors.White}{item.Name} {ChatColors.Grey}- {item.Description}");
|
||||
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
|
||||
ICommandInfo info) {
|
||||
var items = new List<IShopItem>(shop.Items).Where(item
|
||||
=> executor == null
|
||||
|| games.ActiveGame is not { State: State.IN_PROGRESS }
|
||||
|| item.CanPurchase(executor) != PurchaseResult.WRONG_ROLE)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
items.Sort((a, b) => {
|
||||
var aPrice = a.Config.Price;
|
||||
var bPrice = b.Config.Price;
|
||||
var aCanBuy = executor != null
|
||||
&& a.CanPurchase(executor) == PurchaseResult.SUCCESS;
|
||||
var bCanBuy = executor != null
|
||||
&& b.CanPurchase(executor) == PurchaseResult.SUCCESS;
|
||||
|
||||
if (aCanBuy && !bCanBuy) return -1;
|
||||
if (!aCanBuy && bCanBuy) return 1;
|
||||
if (aPrice != bPrice) return aPrice.CompareTo(bPrice);
|
||||
return string.Compare(a.Name, b.Name, StringComparison.Ordinal);
|
||||
});
|
||||
|
||||
var balance = info.CallingPlayer == null ?
|
||||
int.MaxValue :
|
||||
await shop.Load(info.CallingPlayer);
|
||||
|
||||
foreach (var item in items)
|
||||
info.ReplySync(formatItem(item,
|
||||
item.Config.Price <= balance
|
||||
&& item.CanPurchase(info.CallingPlayer ?? executor!)
|
||||
== PurchaseResult.SUCCESS));
|
||||
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
|
||||
private string formatPrefix(IShopItem item, bool canBuy = true) {
|
||||
if (!canBuy)
|
||||
return
|
||||
$" {ChatColors.Grey}- [{ChatColors.DarkRed}{item.Config.Price}{ChatColors.Grey}] {ChatColors.Red}{item.Name}";
|
||||
return
|
||||
$" {ChatColors.Default}- [{ChatColors.Yellow}{item.Config.Price}{ChatColors.Default}] {ChatColors.Green}{item.Name}";
|
||||
}
|
||||
|
||||
private string formatItem(IShopItem item, bool canBuy) {
|
||||
return
|
||||
$" {formatPrefix(item, canBuy)} {ChatColors.Grey} | {item.Description}";
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,18 @@ public class ShopCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly Dictionary<string, ICommand> subcommands = new() {
|
||||
["list"] = new ListCommand(provider),
|
||||
["buy"] = new BuyCommand(provider),
|
||||
["balance"] = new BalanceCommand(provider)
|
||||
["balance"] = new BalanceCommand(provider),
|
||||
["bal"] = new BalanceCommand(provider)
|
||||
};
|
||||
|
||||
public void Dispose() { }
|
||||
public string Id => "shop";
|
||||
public string[] Usage => ["list", "buy [item]", "balance"];
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public bool MustBeOnMainThread => true;
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
HashSet<string> sent = [];
|
||||
|
||||
@@ -18,11 +18,11 @@ public static class StickerExtensions {
|
||||
|
||||
public class Stickers(IServiceProvider provider)
|
||||
: RoleRestrictedItem<DetectiveRole>(provider) {
|
||||
private readonly StickerConfig config = provider
|
||||
.GetService<IStorage<StickerConfig>>()
|
||||
private readonly StickersConfig config = provider
|
||||
.GetService<IStorage<StickersConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new StickerConfig();
|
||||
.GetResult() ?? new StickersConfig();
|
||||
|
||||
private readonly IIconManager? icons = provider.GetService<IIconManager>();
|
||||
|
||||
|
||||
38
TTT/Shop/Items/Healthshot/HealthshotItem.cs
Normal file
38
TTT/Shop/Items/Healthshot/HealthshotItem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.Shop.Items.Healthshot;
|
||||
|
||||
public static class HealthshotServiceCollection {
|
||||
public static void AddHealthshot(this IServiceCollection services) {
|
||||
services.AddModBehavior<HealthshotItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class HealthshotItem(IServiceProvider provider) : BaseItem(provider) {
|
||||
private readonly HealthshotConfig config =
|
||||
provider.GetService<IStorage<HealthshotConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new HealthshotConfig();
|
||||
|
||||
public override string Name => Locale[HealthshotMsgs.SHOP_ITEM_HEALTHSHOT];
|
||||
|
||||
public override string Description
|
||||
=> Locale[HealthshotMsgs.SHOP_ITEM_HEALTHSHOT_DESC];
|
||||
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
|
||||
}
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
return PurchaseResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
11
TTT/Shop/Items/Healthshot/HealthshotMsgs.cs
Normal file
11
TTT/Shop/Items/Healthshot/HealthshotMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Items.Healthshot;
|
||||
|
||||
public class HealthshotMsgs {
|
||||
public static IMsg SHOP_ITEM_HEALTHSHOT
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_HEALTHSHOT));
|
||||
|
||||
public static IMsg SHOP_ITEM_HEALTHSHOT_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_HEALTHSHOT_DESC));
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public class PlayerKillListener(IServiceProvider provider)
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public async Task OnKill(PlayerDeathEvent ev) {
|
||||
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
|
||||
if (ev.Killer == null) return;
|
||||
var victimBal = await shop.Load(ev.Victim);
|
||||
|
||||
@@ -25,7 +25,7 @@ public class PlayerKillListener(IServiceProvider provider)
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
[EventHandler(IgnoreCanceled = true)]
|
||||
public async Task OnIdentify(BodyIdentifyEvent ev) {
|
||||
if (ev.Identifier == null) return;
|
||||
var victimBal = await shop.Load(ev.Body.OfPlayer);
|
||||
@@ -37,12 +37,12 @@ public class PlayerKillListener(IServiceProvider provider)
|
||||
if (!isGoodKill(ev.Body.Killer, ev.Body.OfPlayer)) {
|
||||
var killerBal = await shop.Load(killer);
|
||||
shop.AddBalance(killer, -killerBal / 4,
|
||||
ev.Body.OfPlayer.Name + " kill invalidated");
|
||||
ev.Body.OfPlayer.Name + " Bad Kill");
|
||||
return;
|
||||
}
|
||||
|
||||
shop.AddBalance(killer, victimBal / 4,
|
||||
ev.Body.OfPlayer.Name + " kill validated");
|
||||
ev.Body.OfPlayer.Name + " Good Kill");
|
||||
}
|
||||
|
||||
private bool isGoodKill(IPlayer attacker, IPlayer victim) {
|
||||
|
||||
@@ -13,12 +13,12 @@ public class RoundShopClearer(IServiceProvider provider) : IListener {
|
||||
|
||||
public void Dispose() { bus.UnregisterListener(this); }
|
||||
|
||||
[EventHandler(IgnoreCanceled = true, Priority = Priority.LOW)]
|
||||
[EventHandler(IgnoreCanceled = true)]
|
||||
[UsedImplicitly]
|
||||
public void OnRoundStart(GameStateUpdateEvent ev) {
|
||||
// Only clear balances if the round is in progress
|
||||
// This is called only once, which means the round went from COUNTDOWN / WAITING -> IN_PROGRESS
|
||||
if (ev.NewState != State.IN_PROGRESS) return;
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
shop.ClearBalances();
|
||||
shop.ClearItems();
|
||||
}
|
||||
|
||||
42
TTT/Shop/PeriodicRewarder.cs
Normal file
42
TTT/Shop/PeriodicRewarder.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
|
||||
namespace TTT.Shop;
|
||||
|
||||
public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
private readonly ShopConfig config = provider
|
||||
.GetService<IStorage<ShopConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new ShopConfig(provider);
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IScheduler scheduler =
|
||||
provider.GetRequiredService<IScheduler>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private IDisposable? timer;
|
||||
|
||||
public void Dispose() { timer?.Dispose(); }
|
||||
|
||||
public void Start() {
|
||||
timer = scheduler.SchedulePeriodic(config.CreditRewardInterval,
|
||||
issueRewards);
|
||||
}
|
||||
|
||||
private void issueRewards() {
|
||||
Server.NextWorldUpdate(() => {
|
||||
foreach (var player in finder.GetOnline().Where(p => p.IsAlive))
|
||||
shop.AddBalance(player, config.IntervalRewardAmount, "Time Reward");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,8 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
|
||||
|
||||
public void AddBalance(IOnlinePlayer player, int amount, string reason = "",
|
||||
bool print = true) {
|
||||
messenger?.Debug(
|
||||
$"Adding {amount} to {player.Name} ({player.Id}) balance. Reason: {reason}");
|
||||
if (amount == 0) return;
|
||||
balances.TryAdd(player.Id, 0);
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ using TTT.CS2.Items.BodyPaint;
|
||||
using TTT.CS2.Items.Camouflage;
|
||||
using TTT.CS2.Items.DNA;
|
||||
using TTT.CS2.Items.PoisonShots;
|
||||
using TTT.CS2.Items.PoisonSmoke;
|
||||
using TTT.CS2.Items.Station;
|
||||
using TTT.Shop.Commands;
|
||||
using TTT.Shop.Items;
|
||||
using TTT.Shop.Items.Detective.Stickers;
|
||||
using TTT.Shop.Items.Healthshot;
|
||||
using TTT.Shop.Items.M4A1;
|
||||
using TTT.Shop.Items.Taser;
|
||||
using TTT.Shop.Items.Traitor.C4;
|
||||
@@ -24,6 +26,8 @@ public static class ShopServiceCollection {
|
||||
|
||||
collection.AddModBehavior<RoundShopClearer>();
|
||||
collection.AddModBehavior<RoleAssignCreditor>();
|
||||
collection.AddModBehavior<PlayerKillListener>();
|
||||
collection.AddModBehavior<PeriodicRewarder>();
|
||||
|
||||
collection.AddModBehavior<ShopCommand>();
|
||||
collection.AddModBehavior<BuyCommand>();
|
||||
@@ -38,8 +42,10 @@ public static class ShopServiceCollection {
|
||||
collection.AddDnaScannerServices();
|
||||
collection.AddGlovesServices();
|
||||
collection.AddHealthStation();
|
||||
collection.AddHealthshot();
|
||||
collection.AddM4A1Services();
|
||||
collection.AddPoisonShots();
|
||||
collection.AddPoisonSmoke();
|
||||
collection.AddStickerServices();
|
||||
collection.AddTaserItem();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using ShopAPI;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop;
|
||||
|
||||
public static class ShopMsgs {
|
||||
public static IMsg SHOP_PREFIX => MsgFactory.Create(nameof(SHOP_PREFIX));
|
||||
|
||||
public static IMsg SHOP_INACTIVE => MsgFactory.Create(nameof(SHOP_INACTIVE));
|
||||
|
||||
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
|
||||
@@ -20,15 +23,19 @@ public static class ShopMsgs {
|
||||
}
|
||||
|
||||
public static IMsg CREDITS_GIVEN(int amo) {
|
||||
return MsgFactory.Create(nameof(CREDITS_GIVEN), amo > 0 ? "+" : "-",
|
||||
return MsgFactory.Create(nameof(CREDITS_GIVEN), getCreditPrefix(amo),
|
||||
Math.Abs(amo));
|
||||
}
|
||||
|
||||
public static IMsg CREDITS_GIVEN_REASON(int amo, string reason) {
|
||||
return MsgFactory.Create(nameof(CREDITS_GIVEN_REASON), amo > 0 ? "+" : "-",
|
||||
return MsgFactory.Create(nameof(CREDITS_GIVEN_REASON), getCreditPrefix(amo),
|
||||
Math.Abs(amo), reason);
|
||||
}
|
||||
|
||||
private static string getCreditPrefix(int diff) {
|
||||
return diff > 0 ? ChatColors.Green + "+" : ChatColors.Red + "-";
|
||||
}
|
||||
|
||||
public static IMsg SHOP_INSUFFICIENT_BALANCE(IShopItem item, int bal) {
|
||||
return MsgFactory.Create(nameof(SHOP_INSUFFICIENT_BALANCE), item.Name,
|
||||
item.Config.Price, bal);
|
||||
@@ -37,4 +44,8 @@ public static class ShopMsgs {
|
||||
public static IMsg SHOP_CANNOT_PURCHASE_WITH_REASON(string reason) {
|
||||
return MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE_WITH_REASON), reason);
|
||||
}
|
||||
|
||||
public static IMsg COMMAND_BALANCE(int bal) {
|
||||
return MsgFactory.Create(nameof(COMMAND_BALANCE), bal);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
SHOP_INACTIVE: "%PREFIX%The shop is currently closed."
|
||||
SHOP_ITEM_NOT_FOUND: "%PREFIX%Could not find an item named \"{default}{0}{grey}\"."
|
||||
SHOP_PREFIX: "{green}SHOP {grey}| "
|
||||
SHOP_INACTIVE: "%SHOP_PREFIX%The shop is currently closed."
|
||||
SHOP_ITEM_NOT_FOUND: "%SHOP_PREFIX%Could not find an item named \"{default}{0}{grey}\"."
|
||||
|
||||
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
|
||||
SHOP_ITEM_DEAGLE_DESC: "A one-hit kill revolver with a single bullet. Aim carefully!"
|
||||
@@ -7,7 +8,7 @@ SHOP_ITEM_DEAGLE_HIT_FF: "You hit a teammate!"
|
||||
|
||||
SHOP_ITEM_STICKERS: "Stickers"
|
||||
SHOP_ITEM_STICKERS_DESC: "Reveal the roles of all players you taser to others."
|
||||
SHOP_ITEM_STICKERS_HIT: "%PREFIX%You got stickered, your role is now visible to everyone."
|
||||
SHOP_ITEM_STICKERS_HIT: "%SHOP_PREFIX%You got stickered, your role is now visible to everyone."
|
||||
|
||||
SHOP_ITEM_C4: "C4 Explosive"
|
||||
SHOP_ITEM_C4_DESC: "A powerful explosive that blows up after a delay."
|
||||
@@ -17,18 +18,23 @@ SHOP_ITEM_M4A1_DESC: "A fully automatic rifle with a silencer accompanied by a s
|
||||
|
||||
SHOP_ITEM_GLOVES: "Gloves"
|
||||
SHOP_ITEM_GLOVES_DESC: "Lets you kill without DNA being left behind, or move bodies without identifying the body."
|
||||
SHOP_ITEM_GLOVES_USED_BODY: "%PREFIX%You used your gloves to move a body without leaving DNA. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_USED_KILL: "%PREFIX%You used your gloves to kill without leaving DNA evidence. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_WORN_OUT: "%PREFIX%Your gloves worn out."
|
||||
SHOP_ITEM_GLOVES_USED_BODY: "%SHOP_PREFIX%You used your gloves to move a body without leaving DNA. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_USED_KILL: "%SHOP_PREFIX%You used your gloves to kill without leaving DNA evidence. ({yellow}{0}{grey}/{yellow}{1}{grey} use%s% left)."
|
||||
SHOP_ITEM_GLOVES_WORN_OUT: "%SHOP_PREFIX%Your gloves worn out."
|
||||
|
||||
SHOP_ITEM_TASER: "Taser"
|
||||
SHOP_ITEM_TASER_DESC: "A taser that allows you to identify the roles of players you hit."
|
||||
|
||||
SHOP_INSUFFICIENT_BALANCE: "%PREFIX%You cannot afford {white}{0}{grey}, it costs {yellow}{1}{grey} credit%s%, and you have {yellow}{2}{grey}."
|
||||
SHOP_CANNOT_PURCHASE: "%PREFIX%You cannot purchase this item."
|
||||
SHOP_CANNOT_PURCHASE_WITH_REASON: "%PREFIX%You cannot purchase this item: {red}{0}{grey}."
|
||||
SHOP_PURCHASED: "%PREFIX%You purchased {white}{0}{grey}."
|
||||
SHOP_ITEM_HEALTHSHOT: "Healthshot"
|
||||
SHOP_ITEM_HEALTHSHOT_DESC: "A healthshot that instantly heals you for 50 health."
|
||||
|
||||
SHOP_INSUFFICIENT_BALANCE: "%SHOP_PREFIX%You cannot afford {white}{0}{grey}, it costs {yellow}{1}{grey} %CREDITS_NAME%%s%, and you have {yellow}{2}{grey}."
|
||||
SHOP_CANNOT_PURCHASE: "%SHOP_PREFIX%You cannot purchase this item."
|
||||
SHOP_CANNOT_PURCHASE_WITH_REASON: "%SHOP_PREFIX%You cannot purchase this item: {red}{0}{grey}."
|
||||
SHOP_PURCHASED: "%SHOP_PREFIX%You purchased {white}{0}{grey}."
|
||||
|
||||
CREDITS_NAME: "credit"
|
||||
CREDITS_GIVEN: "%PREFIX%{0}{1} %CREDITS_NAME%%s%"
|
||||
CREDITS_GIVEN_REASON: "%PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"
|
||||
CREDITS_GIVEN: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s%"
|
||||
CREDITS_GIVEN_REASON: "%SHOP_PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"
|
||||
|
||||
COMMAND_BALANCE: "%SHOP_PREFIX%You have {yellow}{0}{grey} %CREDITS_NAME%%s%."
|
||||
@@ -3,7 +3,7 @@ using System.Drawing;
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record BodyPaintConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 60;
|
||||
public override int Price { get; init; } = 40;
|
||||
public int MaxUses { get; init; } = 1;
|
||||
public Color ColorToApply { get; init; } = Color.GreenYellow;
|
||||
}
|
||||
@@ -2,5 +2,5 @@ namespace ShopAPI.Configs;
|
||||
|
||||
public record CamoConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 100;
|
||||
public float CamoVisibility { get; init; } = 0.5f;
|
||||
public float CamoVisibility { get; init; } = 0.4f;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ShopAPI.Configs.Detective;
|
||||
|
||||
public record DnaScannerConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 100;
|
||||
public override int Price { get; init; } = 120;
|
||||
public int MaxSamples { get; init; } = 0;
|
||||
public TimeSpan DecayTime { get; init; } = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
@@ -5,12 +5,14 @@ namespace ShopAPI.Configs.Detective;
|
||||
public record HealthStationConfig : StationConfig {
|
||||
public override string UseSound { get; init; } = "sounds/buttons/blip1";
|
||||
|
||||
public override int Price { get; init; } = 60;
|
||||
|
||||
public override Color GetColor(float health) {
|
||||
// 100% health = white
|
||||
// 10% health = green
|
||||
var r = (int)(255 * (1 - health)); // goes from 255 → 0
|
||||
var g = 255; // stays at 255
|
||||
var b = (int)(255 * (1 - health)); // goes from 255 → 0
|
||||
// 10% health = blue
|
||||
var r = (int)(255 * health);
|
||||
var g = (int)(255 * health);
|
||||
var b = 255;
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace ShopAPI.Configs.Detective;
|
||||
|
||||
public record StickerConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 70;
|
||||
}
|
||||
5
TTT/ShopAPI/Configs/Detective/StickersConfig.cs
Normal file
5
TTT/ShopAPI/Configs/Detective/StickersConfig.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace ShopAPI.Configs.Detective;
|
||||
|
||||
public record StickersConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 30;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record M4A1Config : ShopItemConfig {
|
||||
public override int Price { get; init; } = 90;
|
||||
public override int Price { get; init; } = 85;
|
||||
public int[] ClearSlots { get; init; } = [0, 1];
|
||||
public string[] Weapons { get; init; } = ["m4a1", "usps"];
|
||||
}
|
||||
@@ -27,9 +27,15 @@ public record ShopConfig(IRoleAssigner assigner) {
|
||||
public int CreditsForDetectiveVInnoKill { get; init; } = -6;
|
||||
public int CreditsForDetectiveVTraitorKill { get; init; } = 8;
|
||||
public int CreditsForAnyKill { get; init; } = 2;
|
||||
|
||||
public float CreditMultiplierForAssisting { get; init; } = 0.5f;
|
||||
public float CreditsMultiplierForNotAssisted { get; init; } = 1.5f;
|
||||
|
||||
public TimeSpan CreditRewardInterval { get; init; } =
|
||||
TimeSpan.FromSeconds(30);
|
||||
|
||||
public int IntervalRewardAmount { get; init; } = 8;
|
||||
|
||||
public virtual int CreditsForKill(IOnlinePlayer attacker,
|
||||
IOnlinePlayer victim) {
|
||||
var attackerRole = assigner.GetRoles(attacker)
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Drawing;
|
||||
using ShopAPI.Configs;
|
||||
|
||||
namespace ShopAPI;
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public abstract record StationConfig : ShopItemConfig {
|
||||
public override int Price { get; init; }
|
||||
public virtual int HealthIncrements { get; init; } = 5;
|
||||
public virtual int TotalHealthGiven { get; init; } = 200;
|
||||
public virtual int StationHealth { get; init; } = 100;
|
||||
public virtual int TotalHealthGiven { get; init; } = 0;
|
||||
public virtual int StationHealth { get; init; } = 1000;
|
||||
public virtual float MaxRange { get; init; } = 256;
|
||||
|
||||
public virtual TimeSpan HealthInterval { get; init; } =
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record TaserConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 120;
|
||||
public override int Price { get; init; } = 100;
|
||||
public string Weapon { get; init; } = "taser";
|
||||
}
|
||||
@@ -3,17 +3,19 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record DamageStationConfig : StationConfig {
|
||||
public override int HealthIncrements { get; init; } = -15;
|
||||
public override int TotalHealthGiven { get; init; } = -300;
|
||||
public override int HealthIncrements { get; init; } = -20;
|
||||
public override int TotalHealthGiven { get; init; } = -3000;
|
||||
|
||||
public override string UseSound { get; init; } = "sounds/buttons/blip2";
|
||||
|
||||
public override int Price { get; init; } = 65;
|
||||
|
||||
public override Color GetColor(float health) {
|
||||
// 101% health = white
|
||||
// 100% health = white
|
||||
// 10% health = red
|
||||
var r = 255; // stays at 255
|
||||
var g = (int)(255 * (1 - health)); // goes from 255 → 0
|
||||
var b = (int)(255 * (1 - health)); // goes from 255 → 0
|
||||
var r = 255;
|
||||
var g = (int)(255 * health);
|
||||
var b = (int)(255 * health);
|
||||
return Color.FromArgb(r, g, b);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record GlovesConfig : ShopItemConfig {
|
||||
public override int Price { get; init; }
|
||||
public override int Price { get; init; } = 65;
|
||||
public int MaxUses { get; init; } = 3;
|
||||
}
|
||||
10
TTT/ShopAPI/Configs/Traitor/PoisonConfig.cs
Normal file
10
TTT/ShopAPI/Configs/Traitor/PoisonConfig.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record PoisonConfig {
|
||||
public TimeSpan TimeBetweenDamage { get; init; } = TimeSpan.FromSeconds(2.5);
|
||||
public int DamagePerTick { get; init; } = 5;
|
||||
public int TotalDamage { get; init; } = 60;
|
||||
|
||||
public string PoisonSound { get; init; } =
|
||||
"sounds/player/player_damagebody_03";
|
||||
}
|
||||
@@ -3,14 +3,8 @@ using System.Drawing;
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record PoisonShotsConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 30;
|
||||
public TimeSpan TimeBetweenDamage { get; init; } = TimeSpan.FromSeconds(2);
|
||||
public int DamagePerTick { get; init; } = 5;
|
||||
public int TotalDamage { get; init; } = 50;
|
||||
public override int Price { get; init; } = 65;
|
||||
public int TotalShots { get; init; } = 3;
|
||||
|
||||
public string PoisonSound { get; init; } =
|
||||
"sounds/player/player_damagebody_03";
|
||||
|
||||
public Color PoisonColor { get; init; } = Color.FromArgb(128, Color.Purple);
|
||||
public PoisonConfig PoisonConfig { get; init; } = new();
|
||||
}
|
||||
12
TTT/ShopAPI/Configs/Traitor/PoisonSmokeConfig.cs
Normal file
12
TTT/ShopAPI/Configs/Traitor/PoisonSmokeConfig.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace ShopAPI.Configs.Traitor;
|
||||
|
||||
public record PoisonSmokeConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 30;
|
||||
|
||||
public string Weapon { get; init; } = "smoke";
|
||||
|
||||
public float SmokeRadius { get; init; } = 180;
|
||||
|
||||
public PoisonConfig PoisonConfig { get; init; } =
|
||||
new() { DamagePerTick = 20, TotalDamage = 500 };
|
||||
}
|
||||
8
TTT/ShopAPI/HealthshotConfig.cs
Normal file
8
TTT/ShopAPI/HealthshotConfig.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using ShopAPI.Configs;
|
||||
|
||||
namespace ShopAPI;
|
||||
|
||||
public record HealthshotConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 25;
|
||||
public string Weapon { get; init; } = "weapon_healthshot";
|
||||
}
|
||||
@@ -3,6 +3,8 @@ using ShopAPI;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.Game.Roles;
|
||||
using TTT.Shop.Listeners;
|
||||
using Xunit;
|
||||
|
||||
@@ -17,6 +19,9 @@ public class BalanceClearTest(IServiceProvider provider) {
|
||||
private readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
[Fact]
|
||||
@@ -30,9 +35,29 @@ public class BalanceClearTest(IServiceProvider provider) {
|
||||
|
||||
var game = games.CreateGame();
|
||||
game?.Start();
|
||||
game?.EndGame();
|
||||
|
||||
var newBalance = await shop.Load(player);
|
||||
|
||||
Assert.Equal(0, newBalance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RoleAssignCreditor_ShouldNotBeOverriden_OnGameStart() {
|
||||
bus.RegisterListener(new RoleAssignCreditor(provider));
|
||||
bus.RegisterListener(new RoundShopClearer(provider));
|
||||
var player = TestPlayer.Random();
|
||||
finder.AddPlayer(player);
|
||||
finder.AddPlayer(TestPlayer.Random());
|
||||
|
||||
var game = games.CreateGame();
|
||||
game?.Start();
|
||||
|
||||
var newBalance = await shop.Load(player);
|
||||
|
||||
var expected = 100;
|
||||
if (roles.GetRoles(player).Any(r => r is TraitorRole)) expected = 120;
|
||||
|
||||
Assert.Equal(expected, newBalance);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
|
||||
namespace TTT.Test;
|
||||
|
||||
public class TestPlayer(string id, string name) : IOnlinePlayer {
|
||||
public List<string> Messages { get; } = [];
|
||||
|
||||
[Obsolete(
|
||||
"Roles are now managed via IRoleAssigner. Use IRoleAssigner.GetRoles(IPlayer) instead.")]
|
||||
public ICollection<IRole> Roles { get; } = [];
|
||||
// [Obsolete(
|
||||
// "Roles are now managed via IRoleAssigner. Use IRoleAssigner.GetRoles(IPlayer) instead.")]
|
||||
// public ICollection<IRole> Roles { get; } = [];
|
||||
|
||||
public string Id { get; } = id;
|
||||
public string Name { get; } = name;
|
||||
|
||||
Reference in New Issue
Block a user