mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-06 22:36:35 -08:00
Compare commits
41 Commits
0.13.0-dev
...
0.14.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41c7a788d3 | ||
|
|
5d37e5d1ec | ||
|
|
741f2b8586 | ||
|
|
e08cad21b3 | ||
|
|
ff4c8e76ff | ||
|
|
e8bb8564ad | ||
|
|
58cb208c1d | ||
|
|
a546a8b22e | ||
|
|
84230fd231 | ||
|
|
fdfc0cc3bd | ||
|
|
7fc0f21fa4 | ||
|
|
ed7ad35b85 | ||
|
|
e6009dd75a | ||
|
|
b295fc45a2 | ||
|
|
385f87ad12 | ||
|
|
6aedbeb3fb | ||
|
|
2d078e4dfa | ||
|
|
ff3dd9563e | ||
|
|
8126dfea21 | ||
|
|
e158bbbd77 | ||
|
|
e382302911 | ||
|
|
75690ee64b | ||
|
|
dadd7b31a1 | ||
|
|
697c7f5d6b | ||
|
|
559718621f | ||
|
|
d84e581392 | ||
|
|
640924d2a2 | ||
|
|
d6da16e537 | ||
|
|
c223f3994b | ||
|
|
8d0b4878f1 | ||
|
|
b1155a18ba | ||
|
|
c134289990 | ||
|
|
d8323f4c64 | ||
|
|
a1f5c27660 | ||
|
|
8ed094b8fc | ||
|
|
07fc47803d | ||
|
|
c7b5460b35 | ||
|
|
afe44097c3 | ||
|
|
63c4e9b7d8 | ||
|
|
e317e9418e | ||
|
|
e39b19930c |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -158,7 +158,7 @@ jobs:
|
||||
|
||||
# Build the JSON body. We feed system guidance and the raw changelog
|
||||
# See OpenAI Responses API docs for the schema and output_text helper. :contentReference[oaicite:0]{index=0}
|
||||
jq -Rs --arg sys "You are an expert release-notes writer. Given a list of changes in various formats (e.g: commits, merges, etc.), write Release notes, grouping by features, features, and other pertinent groups where appropriate. Do not include a group if it is not necessary / populated. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative, past tense voice voice. Output valid Markdown only." \
|
||||
jq -Rs --arg sys "You are an expert release-notes writer. Given a list of changes in various formats (e.g: commits, merges, etc.), write release notes intended for reading by the public, grouping by features, features, and other pertinent groups where appropriate. Do not include a group if it is not necessary / populated. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative, past tense voice voice. Output valid Markdown only." \
|
||||
--arg temp "${OPENAI_TEMPERATURE}" \
|
||||
--arg model "${OPENAI_MODEL}" \
|
||||
'{model:$model, temperature: ($temp|tonumber), input:[{role:"system", content:$sys},{role:"user", content:.}]}' CHANGELOG_RAW.md > request.json
|
||||
|
||||
@@ -43,7 +43,7 @@ public partial class StringLocalizer : IMsgLocalizer {
|
||||
private LocalizedString getString(string name, params object[] arguments) {
|
||||
// Get the localized value
|
||||
string value;
|
||||
try { value = localizer[name].Value; } catch (NullReferenceException e) {
|
||||
try { value = localizer[name].Value; } catch (NullReferenceException) {
|
||||
return new LocalizedString(name, name, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -12,7 +12,6 @@ public interface ICommandManager {
|
||||
/// Registers a command with the manager.
|
||||
/// </summary>
|
||||
/// <param name="command">True if the command was successfully registered.</param>
|
||||
[Obsolete("Registration is done via the ServiceProvider now.")]
|
||||
bool RegisterCommand(ICommand command);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
namespace TTT.API.Events;
|
||||
|
||||
public interface IEventBus {
|
||||
[Obsolete("Registration should be done via the ServiceProvider")]
|
||||
void RegisterListener(IListener listener);
|
||||
|
||||
void UnregisterListener(IListener listener);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ using TTT.CS2.lang;
|
||||
using TTT.CS2.Listeners;
|
||||
using TTT.CS2.Player;
|
||||
using TTT.Game;
|
||||
using TTT.Karma;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2;
|
||||
@@ -34,6 +35,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>();
|
||||
@@ -42,6 +44,10 @@ public static class CS2ServiceCollection {
|
||||
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
|
||||
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
|
||||
collection.AddModBehavior<IStorage<M4A1Config>, CS2M4A1Config>();
|
||||
collection.AddModBehavior<IStorage<TaserConfig>, CS2TaserConfig>();
|
||||
collection
|
||||
.AddModBehavior<IStorage<PoisonSmokeConfig>, CS2PoisonSmokeConfig>();
|
||||
collection.AddModBehavior<IStorage<KarmaConfig>, CS2KarmaConfig>();
|
||||
|
||||
// TTT - CS2 Specific optionals
|
||||
collection.AddScoped<ITextSpawner, TextSpawner>();
|
||||
@@ -52,8 +58,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>();
|
||||
@@ -66,6 +72,8 @@ public static class CS2ServiceCollection {
|
||||
collection.AddModBehavior<PlayerStatsTracker>();
|
||||
collection.AddModBehavior<RoundTimerListener>();
|
||||
collection.AddModBehavior<ScreenColorApplier>();
|
||||
collection.AddModBehavior<KarmaBanner>();
|
||||
collection.AddModBehavior<KarmaSyncer>();
|
||||
|
||||
// Commands
|
||||
#if DEBUG
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
@@ -6,12 +7,16 @@ using TTT.API.Player;
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public string Id => "giveitem";
|
||||
public string[] Usage => ["[item] <player>"];
|
||||
|
||||
public Task<CommandResult>
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
@@ -19,15 +24,29 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
if (info.ArgCount == 1) return Task.FromResult(CommandResult.PRINT_USAGE);
|
||||
|
||||
var query = string.Join(" ", info.Args.Skip(1));
|
||||
var query = info.Args[1];
|
||||
var item = searchItem(query);
|
||||
if (item == null) {
|
||||
info.ReplySync($"Item '{query}' not found.");
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
shop.GiveItem(executor, item);
|
||||
info.ReplySync($"Gave item '{item.Name}' to {executor.Name}.");
|
||||
var target = executor;
|
||||
|
||||
Server.NextWorldUpdateAsync(() => {
|
||||
if (info.ArgCount == 3) {
|
||||
var result = finder.GetPlayerByName(info.Args[2]);
|
||||
if (result == null) {
|
||||
info.ReplySync($"Player '{info.Args[2]}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
target = result;
|
||||
}
|
||||
|
||||
shop.GiveItem(target, item);
|
||||
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Command.Test;
|
||||
|
||||
public class IndexCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
public class IndexCommand : ICommand {
|
||||
public string Id => "index";
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -21,7 +21,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
|
||||
subCommands.Add("state", new StateCommand(provider));
|
||||
subCommands.Add("screencolor", new ScreenColorCommand(provider));
|
||||
subCommands.Add("giveitem", new GiveItemCommand(provider));
|
||||
subCommands.Add("index", new IndexCommand(provider));
|
||||
subCommands.Add("index", new IndexCommand());
|
||||
subCommands.Add("showicons", new ShowIconsCommand(provider));
|
||||
subCommands.Add("sethealth", new SetHealthCommand());
|
||||
}
|
||||
|
||||
59
TTT/CS2/Configs/CS2KarmaConfig.cs
Normal file
59
TTT/CS2/Configs/CS2KarmaConfig.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Cvars.Validators;
|
||||
using TTT.API;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Karma;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
|
||||
public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<string> CV_DB_STRING = new(
|
||||
"css_ttt_karma_dbstring", "Database connection string for Karma storage",
|
||||
"Data Source=karma.db");
|
||||
|
||||
public static readonly FakeConVar<int> CV_MIN_KARMA = new("css_ttt_karma_min",
|
||||
"Minimum possible Karma value", 0, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_DEFAULT_KARMA = new(
|
||||
"css_ttt_karma_default", "Default Karma value for new players", 50,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_LOW_KARMA_COMMAND = new(
|
||||
"css_ttt_karma_low_command",
|
||||
"Command executed when a player falls below the Karma threshold (use {0} for player name)",
|
||||
"css_ban #{0} 4320 Your karma is too low!");
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_TIMEOUT_THRESHOLD = new(
|
||||
"css_ttt_karma_timeout_threshold",
|
||||
"Minimum Karma to avoid punishment or timeout effects", 20,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 1000));
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_ROUND_TIMEOUT = new(
|
||||
"css_ttt_karma_round_timeout", "Number of rounds a Karma penalty persists",
|
||||
4, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 100));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
|
||||
plugin.RegisterFakeConVars(this);
|
||||
}
|
||||
|
||||
public Task<KarmaConfig?> Load() {
|
||||
var cfg = new KarmaConfig {
|
||||
DbString = CV_DB_STRING.Value,
|
||||
MinKarma = CV_MIN_KARMA.Value,
|
||||
DefaultKarma = CV_DEFAULT_KARMA.Value,
|
||||
CommandUponLowKarma = CV_LOW_KARMA_COMMAND.Value,
|
||||
KarmaTimeoutThreshold = CV_KARMA_TIMEOUT_THRESHOLD.Value,
|
||||
KarmaRoundTimeout = CV_KARMA_ROUND_TIMEOUT.Value
|
||||
};
|
||||
|
||||
return Task.FromResult<KarmaConfig?>(cfg);
|
||||
}
|
||||
}
|
||||
38
TTT/CS2/Configs/CS2TaserConfig.cs
Normal file
38
TTT/CS2/Configs/CS2TaserConfig.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using CounterStrikeSharp.API.Modules.Cvars.Validators;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Validators;
|
||||
|
||||
namespace TTT.CS2.Configs;
|
||||
|
||||
public class CS2TaserConfig : IStorage<TaserConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_PRICE = new(
|
||||
"css_ttt_shop_taser_price", "Price of the Taser item", 120,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
|
||||
|
||||
public static readonly FakeConVar<string> CV_WEAPON = new(
|
||||
"css_ttt_shop_taser_weapon", "Weapon entity name used for the Taser",
|
||||
"weapon_taser", ConVarFlags.FCVAR_NONE,
|
||||
new ItemValidator(allowMultiple: false));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public void Start() { }
|
||||
|
||||
public void Start(BasePlugin? plugin) {
|
||||
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
|
||||
plugin.RegisterFakeConVars(this);
|
||||
}
|
||||
|
||||
public Task<TaserConfig?> Load() {
|
||||
var cfg = new TaserConfig {
|
||||
Price = CV_PRICE.Value, Weapon = CV_WEAPON.Value
|
||||
};
|
||||
|
||||
return Task.FromResult<TaserConfig?>(cfg);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CS2M4A1Config : IStorage<M4A1Config>, IPluginModule {
|
||||
public static readonly FakeConVar<string> CV_WEAPONS = new(
|
||||
"css_ttt_shop_m4a1_weapons",
|
||||
"Weapons granted with this item (comma-separated names)",
|
||||
"weapon_m4a1,weapon_usp_silencer", ConVarFlags.FCVAR_NONE,
|
||||
"weapon_m4a1_silencer,weapon_usp_silencer", ConVarFlags.FCVAR_NONE,
|
||||
new ItemValidator(allowMultiple: true));
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,19 @@ public static class PlayerExtensions {
|
||||
pawn.SetColor(color);
|
||||
}
|
||||
|
||||
public static void SetArmor(this CCSPlayerController player, int armor,
|
||||
bool withHelmet = false) {
|
||||
if (!player.IsValid) return;
|
||||
var pawn = player.PlayerPawn.Value;
|
||||
if (pawn == null || !pawn.IsValid) return;
|
||||
pawn.ArmorValue = armor;
|
||||
if (withHelmet)
|
||||
if (pawn.ItemServices != null)
|
||||
new CCSPlayer_ItemServices(pawn.ItemServices.Handle).HasHelmet = true;
|
||||
|
||||
Utilities.SetStateChanged(pawn, "CCSPlayerPawn", "m_ArmorValue");
|
||||
}
|
||||
|
||||
public static void ColorScreen(this CCSPlayerController player, Color color,
|
||||
float hold = 0.1f, float fade = 0.2f, FadeFlags flags = FadeFlags.FADE_IN,
|
||||
bool withPurge = true) {
|
||||
|
||||
17
TTT/CS2/GameHandlers/BombPlantSuppressor.cs
Normal file
17
TTT/CS2/GameHandlers/BombPlantSuppressor.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
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,11 +62,16 @@ public class CombatHandler(IServiceProvider provider) : IPluginModule {
|
||||
}
|
||||
|
||||
var killerStats = ev.Attacker?.ActionTrackingServices?.MatchStats;
|
||||
if (killerStats == null) return;
|
||||
killerStats.Kills -= 1;
|
||||
killerStats.Damage -= ev.DmgHealth;
|
||||
|
||||
if (ev.Attacker != null) {
|
||||
Utilities.SetStateChanged(ev.Attacker,
|
||||
"CCSPlayerController_ActionTrackingServices",
|
||||
"m_pActionTrackingServices");
|
||||
if (killerStats == null) return;
|
||||
killerStats.Kills -= 1;
|
||||
killerStats.Damage -= ev.DmgHealth;
|
||||
if (ev.Attacker.ActionTrackingServices != null)
|
||||
ev.Attacker.ActionTrackingServices.NumRoundKills--;
|
||||
Utilities.SetStateChanged(ev.Attacker, "CCSPlayerController",
|
||||
"m_pActionTrackingServices");
|
||||
ev.FireEventToClient(ev.Attacker);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using TTT.API.Player;
|
||||
using TTT.Karma;
|
||||
|
||||
namespace TTT.CS2.GameHandlers;
|
||||
|
||||
public class KarmaSyncer(IServiceProvider provider) : IPluginModule {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IKarmaService? karma = provider.GetService<IKarmaService>();
|
||||
|
||||
private readonly IPlayerFinder players =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
public void Dispose() { }
|
||||
public string Id => nameof(KarmaSyncer);
|
||||
public string Version => GitVersionInformation.FullSemVer;
|
||||
|
||||
public void Start() { }
|
||||
|
||||
[GameEventHandler]
|
||||
public HookResult OnRoundStart(EventRoundStart _, GameEventInfo _1) {
|
||||
if (karma == null) return HookResult.Continue;
|
||||
|
||||
foreach (var p in Utilities.GetPlayers()) {
|
||||
if (!p.IsValid || p.IsBot) continue;
|
||||
|
||||
var apiPlayer = converter.GetPlayer(p);
|
||||
Task.Run(async () => {
|
||||
var pk = await karma.Load(apiPlayer);
|
||||
|
||||
await Server.NextFrameAsync(() => {
|
||||
p.Score = pk;
|
||||
Utilities.SetStateChanged(p, "CCSPlayerController",
|
||||
"m_pActionTrackingServices");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
}
|
||||
35
TTT/CS2/GameHandlers/NameDisplayer.cs
Normal file
35
TTT/CS2/GameHandlers/NameDisplayer.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
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) {
|
||||
if (OperatingSystem.IsWindows()) return;
|
||||
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;
|
||||
|
||||
@@ -27,8 +27,6 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
private static readonly string T_MODEL =
|
||||
"characters/models/tm_phoenix/tm_phoenix.vmdl";
|
||||
|
||||
// private readonly IDictionary<int, IEnumerable<CPointWorldText>> icons =
|
||||
// new Dictionary<int, IEnumerable<CPointWorldText>>();
|
||||
private readonly IEnumerable<CPointWorldText>?[] icons =
|
||||
new IEnumerable<CPointWorldText>[64];
|
||||
|
||||
@@ -79,8 +77,10 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
plugin
|
||||
?.RegisterListener<CounterStrikeSharp.API.Core.Listeners.CheckTransmit>(
|
||||
onTransmit);
|
||||
if (hotReload) OnRoundEnd(null!, null!);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnRoundEnd(EventRoundStart _, GameEventInfo _1) {
|
||||
foreach (var text in Utilities
|
||||
@@ -93,7 +93,7 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
[UsedImplicitly]
|
||||
[EventHandler(IgnoreCanceled = true)]
|
||||
public void OnRoundStart(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.IN_PROGRESS) return;
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
for (var i = 0; i < icons.Length; i++) removeIcon(i);
|
||||
ClearAllVisibility();
|
||||
traitorsThisRound.Clear();
|
||||
@@ -163,6 +163,7 @@ public class RoleIconsHandler(IServiceProvider provider)
|
||||
icons[player.Slot] = roleIcon;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.MONITOR)]
|
||||
public void OnDeath(PlayerDeathEvent ev) {
|
||||
var gamePlayer = players.GetPlayer(ev.Victim);
|
||||
|
||||
@@ -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");
|
||||
|
||||
42
TTT/CS2/Items/Armor/ArmorItem.cs
Normal file
42
TTT/CS2/Items/Armor/ArmorItem.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
|
||||
namespace TTT.CS2.Items.Armor;
|
||||
|
||||
public static class ArmorItemServicesCollection {
|
||||
public static void AddArmorServices(this IServiceCollection collection) {
|
||||
collection.AddModBehavior<ArmorItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ArmorItem(IServiceProvider provider) : BaseItem(provider) {
|
||||
private readonly ArmorConfig config = provider
|
||||
.GetService<IStorage<ArmorConfig>>()
|
||||
?.Load()
|
||||
.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;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
var gamePlayer = converter.GetPlayer(player);
|
||||
if (gamePlayer == null) return;
|
||||
|
||||
gamePlayer.SetArmor(config.Armor, config.Helmet);
|
||||
}
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
return PurchaseResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
11
TTT/CS2/Items/Armor/ArmorMsgs.cs
Normal file
11
TTT/CS2/Items/Armor/ArmorMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.Armor;
|
||||
|
||||
public class ArmorMsgs {
|
||||
public static IMsg SHOP_ITEM_ARMOR
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_ARMOR));
|
||||
|
||||
public static IMsg SHOP_ITEM_ARMOR_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_ARMOR_DESC));
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using TTT.API.Extensions;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Roles;
|
||||
using TTT.Shop.Items.Traitor.BodyPaint;
|
||||
|
||||
namespace TTT.CS2.Items.BodyPaint;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -10,35 +9,26 @@ using TTT.CS2.API;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Game.Events.Body;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Shop.Items.Traitor.BodyPaint;
|
||||
|
||||
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]
|
||||
public void OnPurchase(PlayerPurchaseItemEvent ev) {
|
||||
if (ev.Item is not BodyPaintItem) return;
|
||||
if (ev.Player is not IOnlinePlayer online) return;
|
||||
uses.TryAdd(online, 0);
|
||||
uses[online] += config.MaxUses;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
[EventHandler(Priority = Priority.HIGH)]
|
||||
public void BodyIdentify(BodyIdentifyEvent ev) {
|
||||
if (!bodies.Bodies.TryGetValue(ev.Body, out var body)) return;
|
||||
if (ev.Identifier == null || !usePaint(ev.Identifier)) return;
|
||||
@@ -48,13 +38,17 @@ public class BodyPaintListener(IServiceProvider provider)
|
||||
|
||||
private bool usePaint(IPlayer player) {
|
||||
if (player is not IOnlinePlayer online) return false;
|
||||
if (!uses.TryGetValue(player, out var useCount)) return false;
|
||||
if (!uses.ContainsKey(player)) {
|
||||
if (!shop.HasItem<BodyPaintItem>(online)) return false;
|
||||
uses[player] = config.MaxUses;
|
||||
}
|
||||
|
||||
if (useCount <= 0) return false;
|
||||
uses[player] = useCount - 1;
|
||||
if (uses[player] <= 0) return false;
|
||||
uses[player]--;
|
||||
if (uses[player] > 0) return true;
|
||||
shop.RemoveItem<BodyPaintItem>(online);
|
||||
Messenger.Message(online, Locale[BodyPaintMsgs.SHOP_ITEM_BODY_PAINT_OUT]);
|
||||
uses.Remove(player);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Items.Traitor.BodyPaint;
|
||||
namespace TTT.CS2.Items.BodyPaint;
|
||||
|
||||
public class BodyPaintMsgs {
|
||||
public static IMsg SHOP_ITEM_BODY_PAINT
|
||||
@@ -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;
|
||||
|
||||
40
TTT/CS2/Items/OneHitKnife/OneHitKnife.cs
Normal file
40
TTT/CS2/Items/OneHitKnife/OneHitKnife.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.CS2.Items.OneHitKnife;
|
||||
|
||||
public static class OneHitKnifeServiceCollection {
|
||||
public static void AddOneHitKnifeService(this IServiceCollection services) {
|
||||
services.AddSingleton<OneHitKnife>();
|
||||
services.AddSingleton<OneHitKnifeListener>();
|
||||
}
|
||||
}
|
||||
|
||||
public class OneHitKnife(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider) {
|
||||
private readonly OneHitKnifeConfig config = provider
|
||||
.GetService<IStorage<OneHitKnifeConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new OneHitKnifeConfig();
|
||||
|
||||
public override string Name
|
||||
=> Locale[OneHitKnifeMsgs.SHOP_ITEM_ONE_HIT_KNIFE];
|
||||
|
||||
public override string Description
|
||||
=> Locale[OneHitKnifeMsgs.SHOP_ITEM_ONE_HIT_KNIFE_DESC];
|
||||
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) { }
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
if (Shop.HasItem<OneHitKnife>(player)) return PurchaseResult.ALREADY_OWNED;
|
||||
return base.CanPurchase(player);
|
||||
}
|
||||
}
|
||||
42
TTT/CS2/Items/OneHitKnife/OneHitKnifeListener.cs
Normal file
42
TTT/CS2/Items/OneHitKnife/OneHitKnifeListener.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs.Traitor;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
|
||||
namespace TTT.CS2.Items.OneHitKnife;
|
||||
|
||||
public class OneHitKnifeListener(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly OneHitKnifeConfig config =
|
||||
provider.GetService<IStorage<OneHitKnifeConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new OneHitKnifeConfig();
|
||||
|
||||
[EventHandler]
|
||||
public void OnDamage(PlayerDamagedEvent ev) {
|
||||
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
|
||||
if (ev.Weapon == null || !Tag.KNIVES.Contains(ev.Weapon)) return;
|
||||
|
||||
var attacker = ev.Attacker;
|
||||
var victim = ev.Player;
|
||||
|
||||
if (attacker == null) return;
|
||||
if (!shop.HasItem<OneHitKnife>(attacker)) return;
|
||||
if (victim is not IOnlinePlayer onlineVictim) return;
|
||||
|
||||
var friendly = Roles.GetRoles(attacker)
|
||||
.Any(r => Roles.GetRoles(victim).Contains(r));
|
||||
if (friendly && !config.FriendlyFire) return;
|
||||
|
||||
shop.RemoveItem<OneHitKnife>(attacker);
|
||||
onlineVictim.Health = 0;
|
||||
}
|
||||
}
|
||||
11
TTT/CS2/Items/OneHitKnife/OneHitKnifeMsgs.cs
Normal file
11
TTT/CS2/Items/OneHitKnife/OneHitKnifeMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.OneHitKnife;
|
||||
|
||||
public class OneHitKnifeMsgs {
|
||||
public static IMsg SHOP_ITEM_ONE_HIT_KNIFE
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_ONE_HIT_KNIFE));
|
||||
|
||||
public static IMsg SHOP_ITEM_ONE_HIT_KNIFE_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_ONE_HIT_KNIFE_DESC));
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using TTT.API.Player;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.CS2.Items.PoisonShots;
|
||||
@@ -8,4 +9,11 @@ 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_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,18 +1,35 @@
|
||||
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.PoisonShots;
|
||||
|
||||
public static class PoisonShotServiceCollection {
|
||||
public static void AddPoisonShots(this IServiceCollection services) {
|
||||
services.AddModBehavior<PoisonShotsItem>();
|
||||
services.AddModBehavior<PoisonShotsListener>();
|
||||
}
|
||||
}
|
||||
|
||||
public class PoisonShotsItem(IServiceProvider provider)
|
||||
: RoleRestrictedItem<TraitorRole>(provider) {
|
||||
private readonly PoisonShotsConfig config = provider
|
||||
.GetService<IStorage<PoisonShotsConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new PoisonShotsConfig();
|
||||
|
||||
public override string Name => Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS];
|
||||
|
||||
public override string Description
|
||||
=> Locale[PoisonShotMsgs.SHOP_ITEM_POISON_SHOTS_DESC];
|
||||
|
||||
public override ShopItemConfig Config { get; }
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) { }
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
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 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;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Events.Player;
|
||||
@@ -18,36 +20,41 @@ using TTT.Game.Listeners;
|
||||
namespace TTT.CS2.Items.PoisonShots;
|
||||
|
||||
public class PoisonShotsListener(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
private readonly Dictionary<IPlayer, int> poisonShots = new();
|
||||
|
||||
: BaseListener(provider), IPluginModule {
|
||||
private readonly PoisonShotsConfig config =
|
||||
provider.GetRequiredService<PoisonShotsConfig>();
|
||||
provider.GetService<IStorage<PoisonShotsConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new PoisonShotsConfig();
|
||||
|
||||
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 = [];
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnPurchase(PlayerPurchaseItemEvent ev) {
|
||||
if (ev.Item is not PoisonShotsItem) return;
|
||||
poisonShots.TryAdd(ev.Player, 0);
|
||||
poisonShots[ev.Player] += config.TotalShots;
|
||||
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 _) {
|
||||
if (ev.Userid == null) return HookResult.Continue;
|
||||
var player = converter.GetPlayer(ev.Userid);
|
||||
if (!poisonShots.TryGetValue(player, out var shot) || shot <= 0)
|
||||
if (!Tag.GUNS.Contains(ev.Weapon)) return HookResult.Continue;
|
||||
if (converter.GetPlayer(ev.Userid) is not IOnlinePlayer player)
|
||||
return HookResult.Continue;
|
||||
Server.NextWorldUpdate(() => poisonShots[player]--);
|
||||
var remainingShots = usePoisonShot(player);
|
||||
if (remainingShots == 0)
|
||||
Messenger.Message(player, Locale[PoisonShotMsgs.SHOP_ITEM_POISON_OUT]);
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
@@ -57,7 +64,8 @@ public class PoisonShotsListener(IServiceProvider provider)
|
||||
if (ev.Attacker == null) return;
|
||||
if (!poisonShots.TryGetValue(ev.Attacker, out var shot) || shot <= 0)
|
||||
return;
|
||||
poisonShots[ev.Attacker]--;
|
||||
Messenger.Message(ev.Attacker,
|
||||
Locale[PoisonShotMsgs.SHOP_ITEM_POISON_HIT(ev.Player)]);
|
||||
addPoisonEffect(ev.Player);
|
||||
}
|
||||
|
||||
@@ -68,15 +76,21 @@ 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
|
||||
if (!tickPoison(effect)) timer?.Dispose();
|
||||
timer = scheduler.SchedulePeriodic(config.PoisonConfig.TimeBetweenDamage, ()
|
||||
=> {
|
||||
Server.NextWorldUpdate(() => {
|
||||
if (tickPoison(effect) || timer == null) return;
|
||||
timer.Dispose();
|
||||
poisonTimers.Remove(timer);
|
||||
});
|
||||
});
|
||||
|
||||
poisonTimers.Add(timer);
|
||||
@@ -85,23 +99,39 @@ 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(Color.Purple, 0.2f, 0.3f);
|
||||
gamePlayer?.ColorScreen(config.PoisonColor, 0.2f, 0.3f);
|
||||
gamePlayer?.ExecuteClientCommand("play " + config.PoisonConfig.PoisonSound);
|
||||
|
||||
return effect.DamageGiven < config.TotalDamage;
|
||||
return effect.DamageGiven < config.PoisonConfig.TotalDamage;
|
||||
}
|
||||
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
foreach (var timer in poisonTimers) timer.Dispose();
|
||||
/// <summary>
|
||||
/// Uses a poison shot for the player. Returns the remaining shots, -1 if none
|
||||
/// are available.
|
||||
/// </summary>
|
||||
/// <param name="player"></param>
|
||||
/// <returns></returns>
|
||||
private int usePoisonShot(IOnlinePlayer player) {
|
||||
if (!poisonShots.TryGetValue(player, out var shot) || shot <= 0) {
|
||||
if (!shop.HasItem<PoisonShotsItem>(player)) return -1;
|
||||
poisonShots[player] = config.TotalShots;
|
||||
}
|
||||
|
||||
poisonShots[player]--;
|
||||
if (poisonShots[player] > 0) return poisonShots[player];
|
||||
|
||||
poisonShots.Remove(player);
|
||||
shop.RemoveItem<PoisonShotsItem>(player);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private class PoisonEffect(IPlayer player) {
|
||||
public IPlayer Player { get; init; } = 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);
|
||||
}
|
||||
}
|
||||
118
TTT/CS2/Items/PoisonSmoke/PoisonSmokeListener.cs
Normal file
118
TTT/CS2/Items/PoisonSmoke/PoisonSmokeListener.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
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 TTT.API;
|
||||
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 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() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
[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,15 +9,15 @@ 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 {
|
||||
private static readonly long PROP_SIZE_SQUARED = 500;
|
||||
: RoleRestrictedItem<T>(provider), IPluginModule where T : IRole {
|
||||
private readonly long PROP_SIZE_SQUARED = 500;
|
||||
protected readonly StationConfig _Config = config;
|
||||
|
||||
protected readonly IPlayerConverter<CCSPlayerController> Converter =
|
||||
|
||||
74
TTT/CS2/Listeners/KarmaBanner.cs
Normal file
74
TTT/CS2/Listeners/KarmaBanner.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Roles;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Karma;
|
||||
using TTT.Karma.Events;
|
||||
using TTT.Karma.lang;
|
||||
|
||||
namespace TTT.CS2.Listeners;
|
||||
|
||||
public class KarmaBanner(IServiceProvider provider) : BaseListener(provider) {
|
||||
private readonly KarmaConfig config =
|
||||
provider.GetService<IStorage<KarmaConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new KarmaConfig();
|
||||
|
||||
private readonly IKarmaService karma =
|
||||
provider.GetRequiredService<IKarmaService>();
|
||||
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly Dictionary<IPlayer, DateTime> lastWarned = new();
|
||||
private readonly Dictionary<IPlayer, int> cooldownRounds = new();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.MONITOR, IgnoreCanceled = true)]
|
||||
public void OnKarmaUpdate(KarmaUpdateEvent ev) {
|
||||
if (ev.Karma < config.MinKarma) {
|
||||
issueKarmaBan(ev.Player);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.Karma >= config.KarmaTimeoutThreshold) return;
|
||||
var timeSinceLastWarn = DateTime.UtcNow
|
||||
- lastWarned.GetValueOrDefault(ev.Player, DateTime.MinValue);
|
||||
if (timeSinceLastWarn <= config.KarmaWarningWindow) return;
|
||||
|
||||
issueKarmaWarning(ev.Player);
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.HIGH)]
|
||||
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
|
||||
if (!cooldownRounds.TryGetValue(ev.Player, out var rounds) || rounds <= 0)
|
||||
return;
|
||||
Messenger.Message(ev.Player, Locale[KarmaMsgs.KARMA_WARNING(rounds)]);
|
||||
cooldownRounds[ev.Player]--;
|
||||
if (cooldownRounds[ev.Player] <= 0) cooldownRounds.Remove(ev.Player);
|
||||
ev.Role = new SpectatorRole(Provider);
|
||||
}
|
||||
|
||||
private void issueKarmaBan(IPlayer player) {
|
||||
Server.NextWorldUpdate(() => {
|
||||
var userId = converter.GetPlayer(player);
|
||||
if (userId == null) return;
|
||||
Server.ExecuteCommand(string.Format(config.CommandUponLowKarma,
|
||||
userId.UserId));
|
||||
Task.Run(async () => await karma.Write(player, config.DefaultKarma));
|
||||
});
|
||||
}
|
||||
|
||||
private void issueKarmaWarning(IPlayer player) {
|
||||
cooldownRounds[player] = config.KarmaRoundTimeout;
|
||||
lastWarned[player] = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
54
TTT/CS2/Listeners/KarmaSyncer.cs
Normal file
54
TTT/CS2/Listeners/KarmaSyncer.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Core.Attributes.Registration;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Karma;
|
||||
using TTT.Karma.Events;
|
||||
|
||||
namespace TTT.CS2.Listeners;
|
||||
|
||||
public class KarmaSyncer(IServiceProvider provider)
|
||||
: BaseListener(provider), IPluginModule {
|
||||
private readonly IPlayerConverter<CCSPlayerController> converter =
|
||||
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
|
||||
|
||||
private readonly IKarmaService? karma = provider.GetService<IKarmaService>();
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnKarmaUpdate(KarmaUpdateEvent ev) {
|
||||
if (karma == null) return;
|
||||
|
||||
Server.NextWorldUpdate(() => {
|
||||
var player = converter.GetPlayer(ev.Player);
|
||||
if (player == null) return;
|
||||
|
||||
player.Score = ev.Karma;
|
||||
Utilities.SetStateChanged(player, "CCSPlayerController", "m_iScore");
|
||||
});
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[GameEventHandler]
|
||||
public HookResult OnJoin(EventPlayerConnectFull ev, GameEventInfo _) {
|
||||
if (ev.Userid == null || karma == null) return HookResult.Continue;
|
||||
var player = converter.GetPlayer(ev.Userid);
|
||||
var user = ev.Userid;
|
||||
|
||||
Task.Run(async () => {
|
||||
var karmaValue = await karma.Load(player);
|
||||
await Server.NextWorldUpdateAsync(() => {
|
||||
if (!user.IsValid) return;
|
||||
user.Score = karmaValue;
|
||||
Utilities.SetStateChanged(user, "CCSPlayerController", "m_iScore");
|
||||
});
|
||||
});
|
||||
|
||||
return HookResult.Continue;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ using CounterStrikeSharp.API.Core;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Messages;
|
||||
using TTT.API.Player;
|
||||
using TTT.Game.Events.Body;
|
||||
using TTT.Game.Events.Game;
|
||||
@@ -18,9 +17,6 @@ public class PlayerStatsTracker(IServiceProvider provider) : IListener {
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
private readonly IMessenger messenger =
|
||||
provider.GetRequiredService<IMessenger>();
|
||||
|
||||
private readonly ISet<int> revealedDeaths = new HashSet<int>();
|
||||
|
||||
private readonly IDictionary<int, (int, int)> roundKillsAndAssists =
|
||||
|
||||
@@ -58,19 +58,15 @@ public class RoundTimerListener(IServiceProvider provider)
|
||||
if (ev.NewState == State.FINISHED) endTimer?.Dispose();
|
||||
if (ev.NewState != State.IN_PROGRESS) return;
|
||||
var duration = config.RoundCfg.RoundDuration(ev.Game.Players.Count);
|
||||
Messenger.DebugAnnounce("Total duration: {0} for {1} player", duration,
|
||||
ev.Game.Players.Count);
|
||||
Server.NextWorldUpdate(() => {
|
||||
RoundUtil.SetTimeRemaining((int)duration.TotalSeconds);
|
||||
});
|
||||
Server.NextWorldUpdate(()
|
||||
=> RoundUtil.SetTimeRemaining((int)duration.TotalSeconds));
|
||||
|
||||
endTimer?.Dispose();
|
||||
endTimer = scheduler.Schedule(duration, () => {
|
||||
Server.NextWorldUpdate(() => {
|
||||
Messenger.DebugAnnounce("Time is up!");
|
||||
ev.Game.EndGame(EndReason.TIMEOUT(new InnocentRole(provider)));
|
||||
endTimer = scheduler.Schedule(duration,
|
||||
() => {
|
||||
Server.NextWorldUpdate(()
|
||||
=> ev.Game.EndGame(EndReason.TIMEOUT(new InnocentRole(Provider))));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -24,4 +24,11 @@ public class SpectatorRole(IServiceProvider provider) : IRole {
|
||||
return players.FirstOrDefault(p
|
||||
=> playerConverter.GetPlayer(p) is { Team: CsTeam.Spectator });
|
||||
}
|
||||
|
||||
public void OnAssign(IOnlinePlayer player) {
|
||||
var csPlayer = playerConverter.GetPlayer(player);
|
||||
if (csPlayer is null) return;
|
||||
csPlayer.CommitSuicide(false, true);
|
||||
csPlayer.ChangeTeam(CsTeam.Spectator);
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,15 @@ SHOP_ITEM_BODY_PAINT_DESC: "Paint bodies to make them appear identified."
|
||||
SHOP_ITEM_BODY_PAINT_OUT: "%PREFIX% You ran out of body paint."
|
||||
|
||||
SHOP_ITEM_POISON_SHOTS: "Poison Shots"
|
||||
SHOP_ITEM_POISON_SHOTS_DESC: "Your bullets are coated in a mildly poisonous substance."
|
||||
SHOP_ITEM_POISON_SHOTS_DESC: "Your bullets are coated in a mildly poisonous substance."
|
||||
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."
|
||||
|
||||
SHOP_ITEM_ONE_HIT_KNIFE: "One-Hit Knife"
|
||||
SHOP_ITEM_ONE_HIT_KNIFE_DESC: "Your next knife hit will be a guaranteed kill."
|
||||
@@ -15,7 +15,9 @@ public class IdentifyBodyAction(IRoleAssigner roles, BodyIdentifyEvent ev)
|
||||
|
||||
#endregion
|
||||
|
||||
public IPlayer Player { get; } = ev.Identifier;
|
||||
public IPlayer Player { get; } =
|
||||
ev.Identifier ?? throw new InvalidOperationException();
|
||||
|
||||
public IPlayer? Other { get; } = ev.Body.OfPlayer;
|
||||
|
||||
public IRole? PlayerRole { get; } =
|
||||
|
||||
@@ -9,7 +9,6 @@ public class EventBus(IServiceProvider provider) : IEventBus, ITerrorModule {
|
||||
private readonly Dictionary<Type, List<(object listener, MethodInfo method)>>
|
||||
handlers = new();
|
||||
|
||||
[Obsolete("Registering listeners is deprecated, use DI instead.")]
|
||||
public void RegisterListener(IListener listener) {
|
||||
var dirtyTypes = new HashSet<Type>();
|
||||
appendListener(listener, dirtyTypes);
|
||||
|
||||
@@ -73,7 +73,7 @@ public abstract class EventModifiedMessenger(IServiceProvider provider)
|
||||
PlayerMessageEvent ev) {
|
||||
if (player == null) return await SendMessage(null, msg);
|
||||
|
||||
Bus.Dispatch(ev);
|
||||
await Bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) return false;
|
||||
|
||||
return await SendMessage(player, ev.Message, ev.Args);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using JetBrains.Annotations;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Role;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.Game.Listeners;
|
||||
|
||||
@@ -19,38 +17,4 @@ public class PlayerCausesEndListener(IServiceProvider provider)
|
||||
public void OnLeave(PlayerLeaveEvent ev) {
|
||||
Games.ActiveGame?.CheckEndConditions();
|
||||
}
|
||||
|
||||
private bool getWinningTeam(out IRole? winningTeam) {
|
||||
var game = Games.ActiveGame;
|
||||
winningTeam = null;
|
||||
if (game is null) return false;
|
||||
|
||||
var traitorRole =
|
||||
game.Roles.First(r => r.GetType().IsAssignableTo(typeof(TraitorRole)));
|
||||
var innocentRole =
|
||||
game.Roles.First(r => r.GetType().IsAssignableTo(typeof(InnocentRole)));
|
||||
var detectiveRole = game.Roles.First(r
|
||||
=> r.GetType().IsAssignableTo(typeof(DetectiveRole)));
|
||||
|
||||
var traitorsAlive = game.GetAlive(typeof(TraitorRole)).Count;
|
||||
var nonTraitorsAlive = game.GetAlive().Count - traitorsAlive;
|
||||
var detectivesAlive = game.GetAlive(typeof(DetectiveRole)).Count;
|
||||
|
||||
switch (traitorsAlive) {
|
||||
case 0 when nonTraitorsAlive == 0:
|
||||
winningTeam = null;
|
||||
return true;
|
||||
case > 0 when nonTraitorsAlive == 0:
|
||||
winningTeam = traitorRole;
|
||||
return true;
|
||||
case 0 when nonTraitorsAlive > 0:
|
||||
winningTeam = nonTraitorsAlive == detectivesAlive ?
|
||||
detectiveRole :
|
||||
innocentRole;
|
||||
return true;
|
||||
default:
|
||||
winningTeam = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,6 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TTTConfig();
|
||||
|
||||
private readonly IInventoryManager inventory =
|
||||
provider.GetRequiredService<IInventoryManager>();
|
||||
|
||||
protected readonly IMsgLocalizer Locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
@@ -172,17 +169,16 @@ 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);
|
||||
players.AddRange(online.Where(p
|
||||
=> RoleAssigner.GetRoles(p)
|
||||
.Any(r => r is TraitorRole or DetectiveRole or InnocentRole)));
|
||||
|
||||
State = State.IN_PROGRESS;
|
||||
|
||||
var traitors = ((IGame)this).GetAlive(typeof(TraitorRole)).Count;
|
||||
var nonTraitors = online.Count - traitors;
|
||||
var nonTraitors = players.Count - traitors;
|
||||
Messenger?.MessageAll(Locale[
|
||||
GameMsgs.GAME_STATE_STARTED(traitors, nonTraitors)]);
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace TTT.Karma.Events;
|
||||
public class KarmaUpdateEvent(IPlayer player, int oldKarma, int newKarma)
|
||||
: PlayerEvent(player), ICancelableEvent {
|
||||
public override string Id => "karma.update";
|
||||
public int OldKarma { get; set; } = oldKarma;
|
||||
public int Karma { get; set; } = newKarma;
|
||||
public bool IsCanceled { get; set; }
|
||||
}
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66"/>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9"/>
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0"/>
|
||||
<PackageReference Include="SQLite" Version="3.13.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
30
TTT/Karma/KarmaCommand.cs
Normal file
30
TTT/Karma/KarmaCommand.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Command;
|
||||
using TTT.API.Player;
|
||||
using TTT.Karma.lang;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Karma;
|
||||
|
||||
public class KarmaCommand(IServiceProvider provider) : ICommand {
|
||||
private readonly IKarmaService karma =
|
||||
provider.GetRequiredService<IKarmaService>();
|
||||
|
||||
private readonly IMsgLocalizer locale =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
|
||||
public string Id => "karma";
|
||||
|
||||
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
|
||||
ICommandInfo info) {
|
||||
if (executor == null) return CommandResult.PLAYER_ONLY;
|
||||
|
||||
var value = await karma.Load(executor);
|
||||
|
||||
info.ReplySync(locale[KarmaMsgs.KARMA_COMMAND(value)]);
|
||||
return CommandResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,17 @@ using TTT.API.Player;
|
||||
namespace TTT.Karma;
|
||||
|
||||
public record KarmaConfig {
|
||||
public string DbString { get; init; }
|
||||
public string DbString { get; init; } = "Data Source=karma.db";
|
||||
|
||||
public int MinKarma { get; init; } = 0;
|
||||
public int DefaultKarma { get; init; } = 50;
|
||||
|
||||
public string CommandUponLowKarma { get; init; } = "karmaban {0} Bad Player!";
|
||||
|
||||
public int MinKarma => 0;
|
||||
public int DefaultKarma => 50;
|
||||
public int MaxKarma(IPlayer player) { return 100; }
|
||||
|
||||
public int KarmaTimeoutThreshold { get; init; } = 20;
|
||||
public int KarmaRoundTimeout { get; init; } = 4;
|
||||
|
||||
public TimeSpan KarmaWarningWindow { get; init; } = TimeSpan.FromDays(1);
|
||||
}
|
||||
@@ -2,14 +2,16 @@ using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Role;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Game.Events.Player;
|
||||
using TTT.Game.Listeners;
|
||||
using TTT.Game.Roles;
|
||||
|
||||
namespace TTT.Karma;
|
||||
|
||||
public class KarmaListener(IServiceProvider provider) : IListener {
|
||||
public class KarmaListener(IServiceProvider provider) : BaseListener(provider) {
|
||||
private static readonly int INNO_ON_TRAITOR = 2;
|
||||
private static readonly int TRAITOR_ON_DETECTIVE = 1;
|
||||
private static readonly int INNO_ON_INNO_VICTIM = -1;
|
||||
@@ -28,7 +30,7 @@ public class KarmaListener(IServiceProvider provider) : IListener {
|
||||
private readonly IRoleAssigner roles =
|
||||
provider.GetRequiredService<IRoleAssigner>();
|
||||
|
||||
public void Dispose() { }
|
||||
private readonly Dictionary<IPlayer, int> queuedKarmaUpdates = new();
|
||||
|
||||
[EventHandler]
|
||||
[UsedImplicitly]
|
||||
@@ -36,14 +38,13 @@ public class KarmaListener(IServiceProvider provider) : IListener {
|
||||
|
||||
[EventHandler]
|
||||
[UsedImplicitly]
|
||||
public Task OnKill(PlayerDeathEvent ev) {
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS })
|
||||
return Task.CompletedTask;
|
||||
public void OnKill(PlayerDeathEvent ev) {
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
|
||||
|
||||
var victim = ev.Victim;
|
||||
var killer = ev.Killer;
|
||||
|
||||
if (killer == null) return Task.CompletedTask;
|
||||
if (killer == null) return;
|
||||
|
||||
var victimRole = roles.GetRoles(victim).First();
|
||||
var killerRole = roles.GetRoles(killer).First();
|
||||
@@ -58,30 +59,47 @@ public class KarmaListener(IServiceProvider provider) : IListener {
|
||||
attackerKarmaMultiplier = badKills[killer.Id];
|
||||
}
|
||||
|
||||
if (victimRole is InnocentRole) {
|
||||
if (killerRole is TraitorRole) return Task.CompletedTask;
|
||||
victimKarmaDelta = INNO_ON_INNO_VICTIM;
|
||||
killerKarmaDelta = INNO_ON_INNO;
|
||||
switch (victimRole) {
|
||||
case InnocentRole when killerRole is TraitorRole:
|
||||
return;
|
||||
case InnocentRole:
|
||||
victimKarmaDelta = INNO_ON_INNO_VICTIM;
|
||||
killerKarmaDelta = INNO_ON_INNO;
|
||||
break;
|
||||
case TraitorRole:
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_TRAITOR :
|
||||
INNO_ON_TRAITOR;
|
||||
break;
|
||||
case DetectiveRole:
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_DETECTIVE :
|
||||
INNO_ON_DETECTIVE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (victimRole is TraitorRole)
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_TRAITOR :
|
||||
INNO_ON_TRAITOR;
|
||||
|
||||
if (victimRole is DetectiveRole)
|
||||
killerKarmaDelta = killerRole is TraitorRole ?
|
||||
TRAITOR_ON_DETECTIVE :
|
||||
INNO_ON_DETECTIVE;
|
||||
|
||||
killerKarmaDelta *= attackerKarmaMultiplier;
|
||||
|
||||
return Task.Run(async () => {
|
||||
var newKillerKarma = await karma.Load(killer) + killerKarmaDelta;
|
||||
var newVictimKarma = await karma.Load(victim) + victimKarmaDelta;
|
||||
queuedKarmaUpdates[killer] = queuedKarmaUpdates.GetValueOrDefault(killer, 0)
|
||||
+ killerKarmaDelta;
|
||||
queuedKarmaUpdates[victim] = queuedKarmaUpdates.GetValueOrDefault(victim, 0)
|
||||
+ victimKarmaDelta;
|
||||
}
|
||||
|
||||
await karma.Write(killer, newKillerKarma);
|
||||
await karma.Write(victim, newVictimKarma);
|
||||
});
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public Task OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return Task.CompletedTask;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var (player, karmaDelta) in queuedKarmaUpdates) {
|
||||
tasks.Add(Task.Run(async () => {
|
||||
var newKarma = await karma.Load(player) + karmaDelta;
|
||||
await karma.Write(player, newKarma);
|
||||
}));
|
||||
}
|
||||
|
||||
queuedKarmaUpdates.Clear();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using TTT.API.Extensions;
|
||||
|
||||
namespace TTT.Karma;
|
||||
|
||||
public static class KarmaServiceCollection {
|
||||
public static void AddKarmaService(this IServiceCollection collection) {
|
||||
collection.AddScoped<IKarmaService, KarmaStorage>();
|
||||
collection.AddModBehavior<IKarmaService, KarmaStorage>();
|
||||
collection.AddModBehavior<KarmaListener>();
|
||||
collection.AddModBehavior<KarmaCommand>();
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using Dapper;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MySqlConnector;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
@@ -12,6 +12,7 @@ using TTT.Karma.Events;
|
||||
namespace TTT.Karma;
|
||||
|
||||
public class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
private static readonly bool enableCache = true;
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly KarmaConfig config =
|
||||
@@ -24,31 +25,48 @@ public class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
private IDbConnection? connection;
|
||||
|
||||
public void Start() {
|
||||
connection = new MySqlConnection(config.DbString);
|
||||
connection = new SqliteConnection(config.DbString);
|
||||
connection.Open();
|
||||
|
||||
Task.Run(async () => {
|
||||
if (connection is not { State: ConnectionState.Open })
|
||||
throw new InvalidOperationException(
|
||||
"Storage connection is not initialized.");
|
||||
|
||||
await connection.ExecuteAsync("CREATE TABLE IF NOT EXISTS PlayerKarma ("
|
||||
+ "PlayerId TEXT PRIMARY KEY, " + "Karma INTEGER NOT NULL)");
|
||||
});
|
||||
|
||||
var scheduler = provider.GetRequiredService<IScheduler>();
|
||||
|
||||
Observable.Interval(TimeSpan.FromMinutes(5), scheduler)
|
||||
.Subscribe(_ => updateKarmas());
|
||||
.Subscribe(_ => Task.Run(async () => await updateKarmas()));
|
||||
}
|
||||
|
||||
public Task<int> Load(IPlayer key) {
|
||||
public async Task<int> Load(IPlayer key) {
|
||||
if (enableCache) {
|
||||
karmaCache.TryGetValue(key, out var cachedKarma);
|
||||
if (cachedKarma != 0) return cachedKarma;
|
||||
}
|
||||
|
||||
if (connection is not { State: ConnectionState.Open })
|
||||
throw new InvalidOperationException(
|
||||
"Storage connection is not initialized.");
|
||||
|
||||
return connection.QuerySingleOrDefaultAsync<int>(
|
||||
$"SELECT IFNULL(Karma, {config.DefaultKarma}) FROM PlayerKarma WHERE PlayerId = @PlayerId",
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
$"SELECT COALESCE((SELECT Karma FROM PlayerKarma WHERE PlayerId = @PlayerId), {config.DefaultKarma})",
|
||||
new { PlayerId = key.Id });
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public void Dispose() { connection?.Dispose(); }
|
||||
|
||||
public string Id => nameof(KarmaStorage);
|
||||
public string Version => GitVersionInformation.FullSemVer;
|
||||
|
||||
public async Task Write(IPlayer key, int newData) {
|
||||
if (newData < config.MinKarma || newData > config.MaxKarma(key))
|
||||
if (newData > config.MaxKarma(key))
|
||||
throw new ArgumentOutOfRangeException(nameof(newData),
|
||||
$"Karma must be between {config.MinKarma} and {config.MaxKarma(key)} for player {key.Id}.");
|
||||
$"Karma must be less than {config.MaxKarma(key)} for player {key.Id}.");
|
||||
|
||||
if (!karmaCache.TryGetValue(key, out var oldKarma)) {
|
||||
oldKarma = await Load(key);
|
||||
@@ -58,10 +76,12 @@ public class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
if (oldKarma == newData) return;
|
||||
|
||||
var karmaUpdateEvent = new KarmaUpdateEvent(key, oldKarma, newData);
|
||||
bus.Dispatch(karmaUpdateEvent);
|
||||
await bus.Dispatch(karmaUpdateEvent);
|
||||
if (karmaUpdateEvent.IsCanceled) return;
|
||||
|
||||
karmaCache[key] = newData;
|
||||
|
||||
if (!enableCache) await updateKarmas();
|
||||
}
|
||||
|
||||
private async Task updateKarmas() {
|
||||
@@ -73,7 +93,7 @@ public class KarmaStorage(IServiceProvider provider) : IKarmaService {
|
||||
foreach (var (player, karma) in karmaCache)
|
||||
tasks.Add(connection.ExecuteAsync(
|
||||
"INSERT INTO PlayerKarma (PlayerId, Karma) VALUES (@PlayerId, @Karma) "
|
||||
+ "ON DUPLICATE KEY UPDATE Karma = @Karma",
|
||||
+ "ON CONFLICT(PlayerId) DO UPDATE SET Karma = @Karma",
|
||||
new { PlayerId = player.Id, Karma = karma }));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
11
TTT/Karma/lang/KarmaMsgs.cs
Normal file
11
TTT/Karma/lang/KarmaMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Karma.lang;
|
||||
|
||||
public class KarmaMsgs {
|
||||
public static IMsg KARMA_COMMAND(int karma)
|
||||
=> MsgFactory.Create(nameof(KARMA_COMMAND), karma);
|
||||
|
||||
public static IMsg KARMA_WARNING(int rounds)
|
||||
=> MsgFactory.Create(nameof(KARMA_WARNING), rounds);
|
||||
}
|
||||
2
TTT/Karma/lang/en.yml
Normal file
2
TTT/Karma/lang/en.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
KARMA_COMMAND: "%PREFIX%You have {yellow}{0}{grey} karma."
|
||||
KARMA_WARNING: "%PREFIX%You have {red}very low{grey} karma, and have been forced to sit out for {yellow}{0} {grey}round%s%. Please make sure you read our rules!"
|
||||
@@ -14,6 +14,16 @@
|
||||
<ProjectReference Include="..\Shop\Shop.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9"/>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Ensure all NuGet deps are copied to the publish folder -->
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PublishBaseDirectory>$(MSBuildThisFileDirectory)/../../build</PublishBaseDirectory>
|
||||
<PublishDir>$(PublishBaseDirectory)/TTT</PublishDir>
|
||||
|
||||
@@ -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,13 +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();
|
||||
|
||||
private readonly IIconManager? icons = provider.GetService<IIconManager>();
|
||||
.GetResult() ?? new StickersConfig();
|
||||
|
||||
public override string Name => Locale[StickerMsgs.SHOP_ITEM_STICKERS];
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
37
TTT/Shop/Items/Taser/TaserItem.cs
Normal file
37
TTT/Shop/Items/Taser/TaserItem.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
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.Taser;
|
||||
|
||||
public static class TaserServiceCollection {
|
||||
public static void AddTaserItem(this IServiceCollection collection) {
|
||||
collection.AddModBehavior<TaserItem>();
|
||||
}
|
||||
}
|
||||
|
||||
public class TaserItem(IServiceProvider provider) : BaseItem(provider) {
|
||||
private readonly TaserConfig config =
|
||||
provider.GetService<IStorage<TaserConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new TaserConfig();
|
||||
|
||||
public override string Name => Locale[TaserMsgs.SHOP_ITEM_TASER];
|
||||
public override string Description => Locale[TaserMsgs.SHOP_ITEM_TASER_DESC];
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
// Remove in case they already have it, to allow refresh of recharging taser
|
||||
Inventory.RemoveWeapon(player, config.Weapon);
|
||||
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
|
||||
}
|
||||
|
||||
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
|
||||
return PurchaseResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
11
TTT/Shop/Items/Taser/TaserMsgs.cs
Normal file
11
TTT/Shop/Items/Taser/TaserMsgs.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop.Items.Taser;
|
||||
|
||||
public class TaserMsgs {
|
||||
public static IMsg SHOP_ITEM_TASER
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_TASER));
|
||||
|
||||
public static IMsg SHOP_ITEM_TASER_DESC
|
||||
=> MsgFactory.Create(nameof(SHOP_ITEM_TASER_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();
|
||||
}
|
||||
|
||||
47
TTT/Shop/PeriodicRewarder.cs
Normal file
47
TTT/Shop/PeriodicRewarder.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using CounterStrikeSharp.API;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Configs;
|
||||
using TTT.API;
|
||||
using TTT.API.Game;
|
||||
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 readonly IGameManager games =
|
||||
provider.GetRequiredService<IGameManager>();
|
||||
|
||||
private IDisposable? timer;
|
||||
|
||||
public void Dispose() { timer?.Dispose(); }
|
||||
|
||||
public void Start() {
|
||||
timer = scheduler.SchedulePeriodic(config.CreditRewardInterval,
|
||||
issueRewards);
|
||||
}
|
||||
|
||||
private void issueRewards() {
|
||||
if (games.ActiveGame is not { State: State.IN_PROGRESS }) return;
|
||||
Server.NextWorldUpdate(() => {
|
||||
foreach (var player in finder.GetOnline().Where(p => p.IsAlive))
|
||||
shop.AddBalance(player, config.IntervalRewardAmount, "Alive");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using TTT.API.Extensions;
|
||||
using TTT.CS2.Items.Armor;
|
||||
using TTT.CS2.Items.BodyPaint;
|
||||
using TTT.CS2.Items.Camouflage;
|
||||
using TTT.CS2.Items.DNA;
|
||||
using TTT.CS2.Items.OneHitKnife;
|
||||
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;
|
||||
using TTT.Shop.Items.Traitor.Gloves;
|
||||
using TTT.Shop.Listeners;
|
||||
@@ -21,11 +27,14 @@ public static class ShopServiceCollection {
|
||||
|
||||
collection.AddModBehavior<RoundShopClearer>();
|
||||
collection.AddModBehavior<RoleAssignCreditor>();
|
||||
collection.AddModBehavior<PlayerKillListener>();
|
||||
collection.AddModBehavior<PeriodicRewarder>();
|
||||
|
||||
collection.AddModBehavior<ShopCommand>();
|
||||
collection.AddModBehavior<BuyCommand>();
|
||||
collection.AddModBehavior<BalanceCommand>();
|
||||
|
||||
collection.AddArmorServices();
|
||||
collection.AddBodyPaintServices();
|
||||
collection.AddC4Services();
|
||||
collection.AddCamoServices();
|
||||
@@ -34,7 +43,12 @@ public static class ShopServiceCollection {
|
||||
collection.AddDnaScannerServices();
|
||||
collection.AddGlovesServices();
|
||||
collection.AddHealthStation();
|
||||
collection.AddHealthshot();
|
||||
collection.AddM4A1Services();
|
||||
collection.AddOneHitKnifeService();
|
||||
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,15 +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_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_TASER: "Taser"
|
||||
SHOP_ITEM_TASER_DESC: "A taser that allows you to identify the roles of players you hit."
|
||||
|
||||
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%."
|
||||
7
TTT/ShopAPI/Configs/ArmorConfig.cs
Normal file
7
TTT/ShopAPI/Configs/ArmorConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record ArmorConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 80;
|
||||
public int Armor { get; init; } = 100;
|
||||
public bool Helmet { get; init; } = true;
|
||||
}
|
||||
@@ -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; } =
|
||||
6
TTT/ShopAPI/Configs/TaserConfig.cs
Normal file
6
TTT/ShopAPI/Configs/TaserConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace ShopAPI.Configs;
|
||||
|
||||
public record TaserConfig : ShopItemConfig {
|
||||
public override int Price { get; init; } = 100;
|
||||
public string Weapon { get; init; } = "taser";
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user