Compare commits

...

13 Commits

Author SHA1 Message Date
MSWS
cab156184c Tweak credit penalty 2025-10-02 11:04:09 -07:00
MSWS
9ee69a0b28 feat: Add credits given for killing / identifying +semver:minor
```
- Refactor balance deduction in Shop.cs to use `AddBalance` method for improved consistency and added player notification for successful purchases.
- Enhance BuyCommand.cs by updating Execute method to return Task.FromResult, adding a health check, and modifying item search logic for better accuracy.
- Update PlayerKillListener.cs to extend from BaseListener, integrate ShopAPI, and add methods for handling on-kill and body identification events with balance adjustments.
- Use JetBrains.Annotations in RoleAssignCreditor.cs for potential external or reflective method use, adding UsedImplicitly attribute for OnRoleAssign method.
- Extend ShopMsgs.cs and en.yml with new purchase success messages including item names, improving player feedback.
```
2025-10-02 11:03:03 -07:00
MSWS
e529229200 Update licenses 2025-10-01 23:46:52 -07:00
MSWS
e7dc5c02fe feat: Add camouflage item +semver:minor (resolves #73)
- Add `CamouflageItem` class for managing camouflage items in the game
- Implement `AddCamoServices` in service collection extension for mod behavior
- Introduce configuration loading for camouflage settings with `IStorage<CamoConfig>`
- Provide localization for item name and description
- Implement purchasing logic: visibility settings and ownership check in `CamouflageItem`
- Add "Camouflage" item and description to the language file
- Expand shop services to include camouflage features via `AddCamoServices`
- Establish `CamoConfig` in `ShopAPI` with default pricing and visibility properties
- Create `CamoMsgs` class for managing camouflage item messages
2025-10-01 23:34:05 -07:00
MSWS
522e42a5ff Reformat & Cleanup 2025-10-01 23:26:30 -07:00
MSWS
871500fbdc Cleanup unused lines 2025-10-01 23:21:39 -07:00
MSWS
f023d36aa9 Update CS2 impl 2025-10-01 23:10:58 -07:00
MSWS
a185b217e0 feat: Add traitor gloves item and reorganize configs. (resolves #81)
- Add new "Gloves" item to traitor shop with localization and descriptions
- Allow modification of `Killer` property in `IBody.cs` for enhanced gameplay flexibility
- Reorganize configuration files under relevant directories for better clarity (e.g., HealthStationConfig, DnaScannerConfig)
- Introduce `GlovesListener` class to handle event-driven interactions for the "Gloves" item
- Implement traitor-specific configurations and services for items like C4 and gloves, including default pricing and usage limits
2025-10-01 23:09:43 -07:00
MSWS
dbfd360c6c feat: Add M4A1 shop item +semver:minor (resolves #71)
```
Add M4A1 Item to Shop with Configuration and Localization

- Add TTT/Shop/Items/M4A1/M4A1Msgs.cs to manage localized messages for the M4A1 item.
- Update TTT/Shop/lang/en.yml with a new shop item "M4A1 Rifle and USP-S" and its description.
- Modify TTT/Shop/ShopServiceCollection.cs to include M4A1 services and adjust service ordering for better organization.
- Introduce TTT/ShopAPI/Configs/M4A1Config.cs for setting up the M4A1 item configuration, including price and weapon slots.
- Create TTT/Shop/Items/M4A1/M4A1ShopItem.cs to define the M4A1 shop item with purchase and inventory management logic.
```
2025-10-01 22:47:05 -07:00
MSWS
4a64741a8e Add CS2-specific configuration of C4 item 2025-10-01 22:20:14 -07:00
MSWS
7372ffda45 feat: Add C4 item for TraitorRole in shop +semver:minor (resolves #79)
- Add C4Msgs class for localization of C4 shop item in the Traitor category
- Introduce C4ShopItem class with methods for purchase, limitations, and role restriction handling
- Update ShopServiceCollection to include C4 service for Traitor role
- Introduce C4Config class for configuration of C4 shop items with various properties
- Add English localization entry for "C4 Explosive" shop item with description
2025-10-01 22:16:03 -07:00
MSWS
9ea9c78208 Remove extra spacing in DNA locale 2025-10-01 22:02:59 -07:00
MSWS
85601f1fc0 feat: Implement configurable sound and localization support
- Add configuration for `UseSound` property in `DamageStationConfig` and `HealthStationConfig`
- Implement localization support in `HealthStation.cs` by replacing hardcoded strings with localized messages
- Introduce `UseSound` abstract property in `StationConfig`
- Update localization files with new shop items and descriptions in `lang/en.yml`
- Create `StationMsgs.cs` to manage station-related item messages via a factory pattern
- Enhance `DamageStation.cs` for internationalization and configurable sound settings
- Annotate unused or implicitly used methods in `StickerListener.cs` with `[UsedImplicitly]`
2025-10-01 22:02:12 -07:00
54 changed files with 1079 additions and 126 deletions

View File

@@ -1,15 +1,18 @@
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |
| Package | Version | License Information Origin | License Expression | License Url | Copyright | Authors | Package Project Url |
| ----------------------------------------------------- | -------- | -------------------------- | ------------------ | --------------------------------------- | ----------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| CounterStrikeSharp.API | 1.0.332 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| CounterStrikeSharp.API | 1.0.340 | Expression | GPL-3.0-only | https://licenses.nuget.org/GPL-3.0-only | | Roflmuffin | http://docs.cssharp.dev/ |
| Dapper | 2.1.66 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | 2019 Stack Exchange, Inc. | Sam Saffron,Marc Gravell,Nick Craver | https://github.com/DapperLib/Dapper |
| JetBrains.Annotations | 2025.2.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) 2016-2025 JetBrains s.r.o. | JetBrains | https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Annotations.html |
| Microsoft.Extensions.DependencyInjection.Abstractions | 9.0.7 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Microsoft.Extensions.Localization.Abstractions | 8.0.3 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://asp.net/ |
| Microsoft.NET.Test.Sdk | 17.14.1 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/vstest |
| Microsoft.Reactive.Testing | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| Microsoft.Testing.Extensions.CodeCoverage | 17.14.2 | Unknown | | https://aka.ms/deprecateLicenseUrl | © Microsoft Corporation. All rights reserved. | Microsoft | https://github.com/microsoft/codecoverage |
| MySqlConnector | 2.4.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright 20162024 Bradley Grainger | Bradley Grainger | https://mysqlconnector.net/ |
| System.Reactive | 6.0.1 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) .NET Foundation and Contributors. | .NET Foundation and Contributors | https://github.com/dotnet/reactive |
| System.Text.Json | 8.0.5 | Expression | MIT | https://licenses.nuget.org/MIT | © Microsoft Corporation. All rights reserved. | Microsoft | https://dot.net/ |
| Xunit.DependencyInjection | 10.6.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright © 2019 | Wei Peng | https://github.com/pengweiqhca/Xunit.DependencyInjection/tree/main/src/Xunit.DependencyInjection |
| xunit.runner.visualstudio | 3.1.3 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| xunit.v3 | 3.0.0 | Expression | Apache-2.0 | https://licenses.nuget.org/Apache-2.0 | Copyright (C) .NET Foundation | jnewkirk,bradwilson | |
| YamlDotNet | 16.3.0 | Expression | MIT | https://licenses.nuget.org/MIT | Copyright (c) Antoine Aubry and contributors | Antoine Aubry | https://github.com/aaubry/YamlDotNet/wiki |

View File

@@ -1,34 +1,17 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Locale;
namespace TTT.CS2;
public class CS2Body(IServiceProvider provider, CRagdollProp ragdoll,
IPlayer player) : IBody {
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMsgLocalizer locale =
provider.GetRequiredService<IMsgLocalizer>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public class CS2Body(CRagdollProp ragdoll, IPlayer player) : IBody {
public CRagdollProp Ragdoll { get; } = ragdoll;
public IPlayer OfPlayer { get; } = player;
public bool IsIdentified { get; set; }
public IWeapon? MurderWeapon { get; private set; }
public IPlayer? Killer { get; private set; }
public IPlayer? Killer { get; set; }
public string Id { get; } = ragdoll.Index.ToString();
public DateTime TimeOfDeath { get; } = DateTime.Now;

View File

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Command;
using TTT.API.Extensions;
using TTT.API.Game;
@@ -39,6 +40,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<IStorage<ShopConfig>, CS2ShopConfig>();
collection
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();

View File

@@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;

View File

@@ -2,7 +2,6 @@
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Locale;
namespace TTT.CS2.Command.Test;

View File

@@ -0,0 +1,61 @@
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 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,
new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_WEAPON = new(
"css_ttt_shop_c4_weapon", "Weapon entity name used for the C4", "weapon_c4",
ConVarFlags.FCVAR_NONE, new ItemValidator(allowMultiple: false));
public static readonly FakeConVar<int> CV_MAX_PER_ROUND = new(
"css_ttt_shop_c4_max_per_round",
"Maximum number of C4 that can be purchased per round", 0,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<int> CV_MAX_AT_ONCE = new(
"css_ttt_shop_c4_max_at_once",
"Maximum number of C4 that can be active at once", 1,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 64));
public static readonly FakeConVar<float> CV_POWER = new(
"css_ttt_shop_c4_power", "Explosion power (damage multiplier) of the C4",
100f, ConVarFlags.FCVAR_NONE, new RangeValidator<float>(0f, 10000f));
public static readonly FakeConVar<int> CV_FUSE_TIME = new(
"css_ttt_shop_c4_fuse_time", "Fuse time of the C4 in seconds", 30,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(1, 300));
public static readonly FakeConVar<bool> CV_FRIENDLY_FIRE = new(
"css_ttt_shop_c4_ff", "Whether the C4 damages teammates");
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) { plugin?.RegisterFakeConVars(this); }
public Task<C4Config?> Load() {
var cfg = new C4Config {
Price = CV_PRICE.Value,
Weapon = CV_WEAPON.Value,
MaxC4PerRound = CV_MAX_PER_ROUND.Value,
MaxC4AtOnce = CV_MAX_AT_ONCE.Value,
Power = CV_POWER.Value,
FuseTime = TimeSpan.FromSeconds(CV_FUSE_TIME.Value),
FriendlyFire = CV_FRIENDLY_FIRE.Value
};
return Task.FromResult<C4Config?>(cfg);
}
}

View File

@@ -32,10 +32,7 @@ public class CS2OneShotDeagleConfig : IStorage<OneShotDeagleConfig>,
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public void Start(BasePlugin? plugin) { plugin?.RegisterFakeConVars(this); }
public Task<OneShotDeagleConfig?> Load() {
var cfg = new OneShotDeagleConfig {

View File

@@ -4,6 +4,7 @@ using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes.Registration;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
@@ -27,6 +28,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
public void Dispose() { }
public void Start() { }
[UsedImplicitly]
[GameEventHandler]
public HookResult OnDeath(EventPlayerDeath ev, GameEventInfo _) {
if (games.ActiveGame is not { State: State.IN_PROGRESS })
@@ -36,7 +38,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
player.SetColor(Color.FromArgb(0, 255, 255, 255));
var ragdollBody = makeGameRagdoll(player);
var body = new CS2Body(provider, ragdollBody, converter.GetPlayer(player));
var body = new CS2Body(ragdollBody, converter.GetPlayer(player));
if (ev.Attacker != null && ev.Attacker.IsValid)
body.WithKiller(converter.GetPlayer(ev.Attacker));
@@ -50,6 +52,7 @@ public class BodySpawner(IServiceProvider provider) : IPluginModule {
return HookResult.Continue;
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnStart(EventRoundStart ev, GameEventInfo _) {
Server.NextWorldUpdate(() => {

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.CS2.Items.Camouflage;
public class CamoMsgs {
public static IMsg SHOP_ITEM_CAMO
=> MsgFactory.Create(nameof(SHOP_ITEM_CAMO));
public static IMsg SHOP_ITEM_CAMO_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_CAMO_DESC));
}

View File

@@ -0,0 +1,42 @@
using System.Drawing;
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.Camouflage;
public static class CamoServiceCollection {
public static void AddCamoServices(this IServiceCollection collection) {
collection.AddModBehavior<CamouflageItem>();
}
}
public class CamouflageItem(IServiceProvider provider) : BaseItem(provider) {
private readonly CamoConfig config =
provider.GetService<IStorage<CamoConfig>>()?.Load().GetAwaiter().GetResult()
?? new CamoConfig();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
public override string Name => Locale[CamoMsgs.SHOP_ITEM_CAMO];
public override string Description => Locale[CamoMsgs.SHOP_ITEM_CAMO_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
var gamePlayer = converter.GetPlayer(player);
var alpha = (int)Math.Round(config.CamoVisibility * 255);
gamePlayer?.SetColor(Color.FromArgb(alpha, Color.White));
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return Shop.HasItem<CamouflageItem>(player) ?
PurchaseResult.ALREADY_OWNED :
PurchaseResult.SUCCESS;
}
}

View File

@@ -16,6 +16,15 @@ namespace TTT.CS2.Items.DNA;
public class DnaListener(IServiceProvider provider) : BaseListener(provider) {
private static readonly TimeSpan cooldown = TimeSpan.FromSeconds(15);
private static readonly string[] missingDnaExplanations = {
"the killer used gloves... for their bullets",
"the killer was very careful", "the killer wiped the weapon clean",
"the killer retrieved the bullets", "the bullets disintegrated on impact",
"the killer was GOATed", "but no DNA was found",
"but legal litigation caused the DNA to be lost",
"and confirmed they were dead", "and they will remember that", "good job"
};
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
@@ -28,15 +37,6 @@ public class DnaListener(IServiceProvider provider) : BaseListener(provider) {
private readonly Dictionary<string, DateTime> lastMessages = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private static readonly string[] missingDnaExplanations = {
"the killer used gloves... for their bullets",
"the killer was very careful", "the killer wiped the weapon clean",
"the killer retrieved the bullets", "the bullets disintegrated on impact",
"the killer was GOATed", "but no DNA was found",
"but legal litigation caused the DNA to be lost",
"and confirmed they were dead", "and they will remember that", "good job"
};
// Low priority to allow body identification to happen first
[UsedImplicitly]
[EventHandler(Priority = Priority.LOW)]

View File

@@ -1,6 +1,5 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;

View File

@@ -1,5 +1,4 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Extensions;
@@ -22,20 +21,20 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()) {
public override string Name => "Damage Station";
public override string Description
=> "Deployable damage station that harms nearby traitors over time.";
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HURT];
public override string Description
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
override protected void onInterval() {
var players = finder.GetOnline();
foreach (var (prop, info) in props) {
@@ -68,7 +67,7 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.ExecuteClientCommand("play sounds/buttons/blip2");
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}

View File

@@ -18,10 +18,10 @@ public class HealthStation(IServiceProvider provider) : StationItem(provider,
?.Load()
.GetAwaiter()
.GetResult() ?? new HealthStationConfig()) {
public override string Name => "Health Station";
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH];
public override string Description
=> "Deployable health station that heals nearby players over time.";
=> Locale[StationMsgs.SHOP_ITEM_STATION_HEALTH_DESC];
override protected void onInterval() {
var players = Utilities.GetPlayers();
@@ -50,7 +50,7 @@ public class HealthStation(IServiceProvider provider) : StationItem(provider,
player.SetHealth(newHealth);
info.HealthGiven += healAmount;
player.ExecuteClientCommand("play sounds/buttons/blip1");
player.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}

View File

@@ -2,6 +2,7 @@
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;
@@ -17,24 +18,25 @@ namespace TTT.CS2.Items.Station;
public abstract class StationItem(IServiceProvider provider,
StationConfig config)
: RoleRestrictedItem<DetectiveRole>(provider), IPluginModule {
private static readonly long PROP_SIZE_SQUARED = 500;
protected readonly StationConfig _Config = config;
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
protected readonly IPlayerConverter<CCSPlayerController> Converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
new();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
private static readonly long PROP_SIZE_SQUARED = 500;
protected readonly Dictionary<CPhysicsPropMultiplayer, StationInfo> props =
new();
private readonly IScheduler scheduler =
provider.GetRequiredService<IScheduler>();
private IDisposable? intervalHandle;
public override ShopItemConfig Config => _Config;
public override void Start() {
base.Start();
intervalHandle = scheduler.SchedulePeriodic(_Config.HealthInterval,
@@ -51,16 +53,18 @@ public abstract class StationItem(IServiceProvider provider,
});
}
public override ShopItemConfig Config => _Config;
public override void Dispose() {
base.Dispose();
intervalHandle?.Dispose();
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnBulletImpact(EventBulletImpact ev, GameEventInfo info) {
var hitVec =
new CounterStrikeSharp.API.Modules.Utils.Vector(ev.X, ev.Y, ev.Z);
var hitVec = new Vector(ev.X, ev.Y, ev.Z);
var nearest = props
.Select(kv => (Key: kv.Key, Value: kv.Value,
.Select(kv => (kv.Key, kv.Value,
Distance: kv.Key.AbsOrigin!.DistanceSquared(hitVec)))
.Where(t => t.Key is { IsValid: true, AbsOrigin: not null })
.OrderBy(t => t.Distance)
@@ -88,8 +92,7 @@ public abstract class StationItem(IServiceProvider provider,
var user = ev.Userid;
var weapon = user?.Pawn.Value?.WeaponServices?.ActiveWeapon.Value;
var dist =
(user?.Pawn.Value?.AbsOrigin?.Distance(
new CounterStrikeSharp.API.Modules.Utils.Vector(ev.X, ev.Y, ev.Z))
(user?.Pawn.Value?.AbsOrigin?.Distance(new Vector(ev.X, ev.Y, ev.Z))
?? null) ?? 1;
var distScale = Math.Clamp(256 / dist, 0.1, 1);
var baseDamage = getBaseDamage(weapon?.DesignerName ?? "");
@@ -129,9 +132,10 @@ public abstract class StationItem(IServiceProvider provider,
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null) {
if (gamePlayer.PlayerPawn.Value != null)
spawnPos += gamePlayer.PlayerPawn.Value?.EyeAngles.ToForward()! * 8;
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
prop.Teleport(spawnPos);
@@ -139,9 +143,4 @@ public abstract class StationItem(IServiceProvider provider,
}
abstract protected void onInterval();
public override void Dispose() {
base.Dispose();
intervalHandle?.Dispose();
}
}

View File

@@ -0,0 +1,17 @@
using TTT.Locale;
namespace TTT.CS2.Items.Station;
public class StationMsgs {
public static IMsg SHOP_ITEM_STATION_HEALTH
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HEALTH));
public static IMsg SHOP_ITEM_STATION_HEALTH_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HEALTH_DESC));
public static IMsg SHOP_ITEM_STATION_HURT
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HURT));
public static IMsg SHOP_ITEM_STATION_HURT_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_STATION_HURT_DESC));
}

View File

@@ -7,7 +7,6 @@ using TTT.API.Player;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.CS2.Extensions;
using TTT.Game;
using TTT.Game.Events.Body;
using TTT.Game.lang;
using TTT.Game.Listeners;

View File

@@ -1,7 +1,6 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using TTT.API.Player;
using TTT.CS2.Extensions;
namespace TTT.CS2.Player;

View File

@@ -1,6 +1,5 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;

View File

@@ -1,8 +1,17 @@
ROLE_SPECTATOR: "Spectator"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
DNA_PREFIX: " {darkblue}D{blue}N{lightblue}A{grey} | {grey}"
DNA_PREFIX: "{darkblue}D{blue}N{lightblue}A{grey} | {grey}"
SHOP_ITEM_DNA: "DNA Scanner"
SHOP_ITEM_DNA_DESC: "Scan bodies to reveal the person who killed them."
SHOP_ITEM_DNA_SCANNED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, their killer was {red}{2}{grey}."
SHOP_ITEM_DNA_SCANNED_OTHER: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, {2}."
SHOP_ITEM_DNA_EXPIRED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, but the DNA has expired."
SHOP_ITEM_DNA_EXPIRED: "%DNA_PREFIX%You scanned {0}{1}'%s% {grey}body, but the DNA has expired."
SHOP_ITEM_STATION_HEALTH: "Health Station"
SHOP_ITEM_STATION_HEALTH_DESC: "A health station that heals players around it."
SHOP_ITEM_STATION_HURT: "Hurt Station"
SHOP_ITEM_STATION_HURT_DESC: "A station that hurts non-Traitors around it."
SHOP_ITEM_CAMO: "Camouflage"
SHOP_ITEM_CAMO_DESC: "Disguise yourself and make yourself harder to see."

View File

@@ -7,7 +7,7 @@ public interface IBody {
IPlayer OfPlayer { get; }
bool IsIdentified { get; set; }
IWeapon? MurderWeapon { get; }
IPlayer? Killer { get; }
IPlayer? Killer { get; set; }
string Id { get; }
DateTime TimeOfDeath { get; }
}

View File

@@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
@@ -22,32 +21,34 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
public void Start() { }
public string[] Aliases => [Id, "purchase", "b"];
public async Task<CommandResult> Execute(IOnlinePlayer? executor,
public Task<CommandResult> Execute(IOnlinePlayer? executor,
ICommandInfo info) {
if (executor == null) {
info.ReplySync("You must be a player to buy items.");
return CommandResult.PLAYER_ONLY;
}
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (games.ActiveGame is not { State: State.IN_PROGRESS }) {
info.ReplySync(locale[ShopMsgs.SHOP_INACTIVE]);
return CommandResult.SUCCESS;
return Task.FromResult(CommandResult.SUCCESS);
}
if (info.ArgCount == 1) return CommandResult.PRINT_USAGE;
if (info.ArgCount == 1) return Task.FromResult(CommandResult.PRINT_USAGE);
if (executor.Health <= 0) {
info.ReplySync(locale[ShopMsgs.SHOP_INACTIVE]);
return Task.FromResult(CommandResult.SUCCESS);
}
var query = string.Join(" ", info.Args.Skip(1));
var item = searchItem(query);
if (item == null) {
info.ReplySync(locale[ShopMsgs.SHOP_ITEM_NOT_FOUND(query)]);
return CommandResult.ERROR;
return Task.FromResult(CommandResult.ERROR);
}
var result = shop.TryPurchase(executor, item);
return result == PurchaseResult.SUCCESS ?
return Task.FromResult(result == PurchaseResult.SUCCESS ?
CommandResult.SUCCESS :
CommandResult.ERROR;
CommandResult.ERROR);
}
private IShopItem? searchItem(string query) {
@@ -62,7 +63,7 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
if (item != null) return item;
item = shop.Items.FirstOrDefault(it
=> it.Name.Contains(query, StringComparison.OrdinalIgnoreCase));
=> it.Description.Contains(query, StringComparison.OrdinalIgnoreCase));
return item;
}

View File

@@ -2,7 +2,6 @@ using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;

View File

@@ -1,4 +1,5 @@
using CounterStrikeSharp.API.Core;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
@@ -17,6 +18,7 @@ public class StickerListener(IServiceProvider provider)
private readonly IIconManager? icons = provider.GetService<IIconManager>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler(Priority = Priority.MONITOR)]
public void OnHurt(PlayerDamagedEvent ev) {
if (icons == null || ev.Attacker == null

View File

@@ -0,0 +1,11 @@
using TTT.Locale;
namespace TTT.Shop.Items.M4A1;
public class M4A1Msgs {
public static IMsg SHOP_ITEM_M4A1
=> MsgFactory.Create(nameof(SHOP_ITEM_M4A1));
public static IMsg SHOP_ITEM_M4A1_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_M4A1_DESC));
}

View File

@@ -0,0 +1,40 @@
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.M4A1;
public static class M4A1ServiceCollection {
public static void AddM4A1Services(this IServiceCollection collection) {
collection.AddModBehavior<M4A1ShopItem>();
}
}
public class M4A1ShopItem(IServiceProvider provider) : BaseItem(provider) {
private readonly M4A1Config config =
provider.GetService<IStorage<M4A1Config>>()?.Load().GetAwaiter().GetResult()
?? new M4A1Config();
public override string Name => Locale[M4A1Msgs.SHOP_ITEM_M4A1];
public override string Description => Locale[M4A1Msgs.SHOP_ITEM_M4A1_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) {
Task.Run(async () => {
foreach (var slot in config.ClearSlots)
await Inventory.RemoveWeaponInSlot(player, slot);
foreach (var weapon in config.Weapons)
await Inventory.GiveWeapon(player, new BaseWeapon(weapon));
});
}
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return PurchaseResult.SUCCESS;
}
}

View File

@@ -0,0 +1,10 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.C4;
public class C4Msgs {
public static IMsg SHOP_ITEM_C4_DESC =
MsgFactory.Create(nameof(SHOP_ITEM_C4_DESC));
public static IMsg SHOP_ITEM_C4 => MsgFactory.Create(nameof(SHOP_ITEM_C4));
}

View File

@@ -0,0 +1,64 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
using TTT.API.Events;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Game.Roles;
namespace TTT.Shop.Items.Traitor.C4;
public static class C4ServiceCollection {
public static void AddC4Services(this IServiceCollection collection) {
collection.AddModBehavior<C4ShopItem>();
}
}
public class C4ShopItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IListener {
private readonly C4Config config = provider.GetService<IStorage<C4Config>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new C4Config();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
private int c4sBought;
public override string Name => Locale[C4Msgs.SHOP_ITEM_C4];
public override string Description => Locale[C4Msgs.SHOP_ITEM_C4_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) {
if (config.MaxC4PerRound > 0)
if (c4sBought > config.MaxC4PerRound)
return PurchaseResult.ITEM_NOT_PURCHASABLE;
if (config.MaxC4AtOnce > 0) {
var count = 0;
if (finder.GetOnline()
.Where(p => Shop.HasItem<C4ShopItem>(p))
.Any(_ => count++ >= config.MaxC4AtOnce))
return PurchaseResult.ITEM_NOT_PURCHASABLE;
}
return base.CanPurchase(player);
}
[UsedImplicitly]
[EventHandler]
public void OnRoundStart(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
c4sBought = 0;
}
}

View File

@@ -0,0 +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.Shop.Items.Traitor.Gloves;
public static class GlovesServiceCollection {
public static void AddGlovesServices(this IServiceCollection collection) {
collection.AddModBehavior<GlovesItem>();
collection.AddModBehavior<GlovesListener>();
}
}
public class GlovesItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly GlovesConfig config =
provider.GetService<IStorage<GlovesConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new GlovesConfig();
public override string Name => Locale[GlovesMsgs.SHOP_ITEM_GLOVES];
public override string Description
=> Locale[GlovesMsgs.SHOP_ITEM_GLOVES_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
}

View File

@@ -0,0 +1,75 @@
using JetBrains.Annotations;
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.Body;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
using TTT.Shop.Events;
namespace TTT.Shop.Items.Traitor.Gloves;
public class GlovesListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly GlovesConfig item =
provider.GetService<IStorage<GlovesConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new GlovesConfig();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly Dictionary<IPlayer, int> uses = new();
[UsedImplicitly]
[EventHandler]
public void OnPurchase(PlayerPurchaseItemEvent ev) {
if (ev.Item is not GlovesItem) return;
uses[ev.Player] = item.MaxUses;
}
[UsedImplicitly]
[EventHandler]
public void BodyCreate(BodyCreateEvent ev) {
if (ev.Body.Killer == null || !useGloves(ev.Body.Killer)) return;
if (ev.Body.Killer is not IOnlinePlayer online) return;
ev.Body.Killer = null;
Messenger.Message(online,
Locale[
GlovesMsgs.SHOP_ITEM_GLOVES_USED_KILL(uses[online], item.MaxUses)]);
}
[UsedImplicitly]
[EventHandler]
public void BodyIdentify(BodyIdentifyEvent ev) {
if (ev.Identifier == null || !useGloves(ev.Identifier)) return;
ev.IsCanceled = true;
Messenger.Message(ev.Identifier,
Locale[
GlovesMsgs.SHOP_ITEM_GLOVES_USED_BODY(uses[ev.Identifier],
item.MaxUses)]);
}
[UsedImplicitly]
[EventHandler]
public void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
uses.Clear();
}
private bool useGloves(IPlayer player) {
uses.TryGetValue(player, out var useCount);
if (useCount <= 0) return false;
uses[player] = useCount - 1;
if (useCount - 1 > 0) return true;
if (player is not IOnlinePlayer online) return true;
shop.RemoveItem<GlovesItem>(online);
Messenger.Message(online, Locale[GlovesMsgs.SHOP_ITEM_GLOVES_WORN_OUT]);
return true;
}
}

View File

@@ -0,0 +1,24 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.Gloves;
public class GlovesMsgs {
public static IMsg SHOP_ITEM_GLOVES
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES));
public static IMsg SHOP_ITEM_GLOVES_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_DESC));
public static IMsg SHOP_ITEM_GLOVES_WORN_OUT
=> MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_WORN_OUT));
public static IMsg SHOP_ITEM_GLOVES_USED_BODY(int usesLeft, int maxUses) {
return MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_USED_BODY), usesLeft,
maxUses);
}
public static IMsg SHOP_ITEM_GLOVES_USED_KILL(int usesLeft, int maxUses) {
return MsgFactory.Create(nameof(SHOP_ITEM_GLOVES_USED_KILL), usesLeft,
maxUses);
}
}

View File

@@ -1,17 +1,51 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game.Events.Body;
using TTT.Game.Events.Player;
using TTT.Game.Listeners;
namespace TTT.Shop.Listeners;
public class PlayerKillListener(IServiceProvider provider) : IListener {
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
public void Dispose() { bus.UnregisterListener(this); }
public class PlayerKillListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnKill(PlayerDeathEvent ev) { }
public async Task OnKill(PlayerDeathEvent ev) {
if (Games.ActiveGame is { State: State.IN_PROGRESS }) return;
if (ev.Killer == null) return;
var victimBal = await shop.Load(ev.Victim);
shop.AddBalance(ev.Killer, victimBal / 6, "Killed " + ev.Victim.Name);
}
[UsedImplicitly]
[EventHandler]
public async Task OnIdentify(BodyIdentifyEvent ev) {
if (ev.Identifier == null) return;
var victimBal = await shop.Load(ev.Body.OfPlayer);
shop.AddBalance(ev.Identifier, victimBal / 4,
"Identified " + ev.Body.OfPlayer.Name);
if (ev.Body.Killer is not IOnlinePlayer killer) return;
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");
return;
}
shop.AddBalance(killer, victimBal / 4,
ev.Body.OfPlayer.Name + " kill validated");
}
private bool isGoodKill(IPlayer attacker, IPlayer victim) {
return !Roles.GetRoles(attacker).Intersect(Roles.GetRoles(victim)).Any();
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
@@ -17,6 +18,7 @@ public class RoleAssignCreditor(IServiceProvider provider)
private readonly IShop shop = provider.GetRequiredService<IShop>();
[UsedImplicitly]
[EventHandler]
public void OnRoleAssign(PlayerRoleAssignEvent ev) {
var toGive = config.StartingCreditsForRole(ev.Role);

View File

@@ -54,8 +54,11 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
return canPurchase;
}
balances[player.Id] = bal - cost;
AddBalance(player, -cost, item.Name);
GiveItem(player, item);
if (printReason)
messenger?.Message(player, localizer[ShopMsgs.SHOP_PURCHASED(item)]);
return PurchaseResult.SUCCESS;
}

View File

@@ -2,8 +2,8 @@
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xml:space="preserve">
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=items_005Coneshotdeagle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=lang/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=shop/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,11 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Extensions;
using TTT.CS2.Items.Camouflage;
using TTT.CS2.Items.DNA;
using TTT.CS2.Items.Station;
using TTT.Shop.Commands;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
using TTT.Shop.Items.M4A1;
using TTT.Shop.Items.Traitor.C4;
using TTT.Shop.Items.Traitor.Gloves;
using TTT.Shop.Listeners;
namespace TTT.Shop;
@@ -21,10 +25,14 @@ public static class ShopServiceCollection {
collection.AddModBehavior<BuyCommand>();
collection.AddModBehavior<BalanceCommand>();
collection.AddDeagleServices();
collection.AddStickerServices();
collection.AddDnaScannerServices();
collection.AddHealthStation();
collection.AddC4Services();
collection.AddCamoServices();
collection.AddDamageStation();
collection.AddDeagleServices();
collection.AddDnaScannerServices();
collection.AddGlovesServices();
collection.AddHealthStation();
collection.AddM4A1Services();
collection.AddStickerServices();
}
}

View File

@@ -6,14 +6,18 @@ namespace TTT.Shop;
public static class ShopMsgs {
public static IMsg SHOP_INACTIVE => MsgFactory.Create(nameof(SHOP_INACTIVE));
public static IMsg SHOP_ITEM_NOT_FOUND(string query)
=> MsgFactory.Create(nameof(SHOP_ITEM_NOT_FOUND), query);
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
public static IMsg SHOP_CANNOT_PURCHASE
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));
public static IMsg SHOP_PURCHASED(IShopItem item)
=> MsgFactory.Create(nameof(SHOP_PURCHASED), item.Name);
public static IMsg SHOP_ITEM_NOT_FOUND(string query) {
return MsgFactory.Create(nameof(SHOP_ITEM_NOT_FOUND), query);
}
public static IMsg CREDITS_GIVEN(int amo) {
return MsgFactory.Create(nameof(CREDITS_GIVEN), amo > 0 ? "+" : "-",
Math.Abs(amo));

View File

@@ -9,9 +9,23 @@ 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_C4: "C4 Explosive"
SHOP_ITEM_C4_DESC: "A powerful explosive that blows up after a delay."
SHOP_ITEM_M4A1: "M4A1 Rifle and USP-S"
SHOP_ITEM_M4A1_DESC: "A fully automatic rifle with a silencer accompanied by a silenced pistol."
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_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}."
CREDITS_NAME: "credit"
CREDITS_GIVEN: "%PREFIX%{0}{1} %CREDITS_NAME%%s%"
CREDITS_GIVEN_REASON: "%PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"

View File

@@ -0,0 +1,6 @@
namespace ShopAPI.Configs;
public record CamoConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public float CamoVisibility { get; init; } = 0.5f;
}

View File

@@ -3,6 +3,8 @@
namespace ShopAPI.Configs;
public record HealthStationConfig : StationConfig {
public override string UseSound { get; init; } = "sounds/buttons/blip1";
public override Color GetColor(float health) {
// 100% health = white
// 10% health = green

View File

@@ -0,0 +1,7 @@
namespace ShopAPI.Configs;
public record M4A1Config : ShopItemConfig {
public override int Price { get; init; } = 90;
public int[] ClearSlots { get; init; } = [0, 1];
public string[] Weapons { get; init; } = ["m4a1", "usps"];
}

View File

@@ -0,0 +1,12 @@
namespace ShopAPI.Configs.Traitor;
// TODO: Support this config
public record C4Config : ShopItemConfig {
public override int Price { get; init; } = 140;
public string Weapon { get; init; } = "c4";
public int MaxC4PerRound { get; init; } = 0;
public int MaxC4AtOnce { get; init; } = 1;
public float Power { get; init; } = 100f;
public TimeSpan FuseTime { get; init; } = TimeSpan.FromSeconds(30);
public bool FriendlyFire { get; init; } = false;
}

View File

@@ -6,6 +6,8 @@ public record DamageStationConfig : StationConfig {
public override int HealthIncrements { get; init; } = -15;
public override int TotalHealthGiven { get; init; } = -300;
public override string UseSound { get; init; } = "sounds/buttons/blip2";
public override Color GetColor(float health) {
// 101% health = white
// 10% health = red

View File

@@ -0,0 +1,6 @@
namespace ShopAPI.Configs.Traitor;
public record GlovesConfig : ShopItemConfig {
public override int Price { get; init; }
public int MaxUses { get; init; } = 3;
}

View File

@@ -30,4 +30,9 @@ public interface IShop : IKeyedStorage<IPlayer, int>,
}
void RemoveItem(IOnlinePlayer player, IShopItem item);
void RemoveItem<T>(IOnlinePlayer player) where T : IShopItem {
var owned = GetOwnedItems(player).FirstOrDefault(i => i is T);
if (owned != null) RemoveItem(player, owned);
}
}

View File

@@ -13,5 +13,6 @@ public abstract record StationConfig : ShopItemConfig {
public virtual TimeSpan HealthInterval { get; init; } =
TimeSpan.FromSeconds(1);
public abstract string UseSound { get; init; }
public abstract Color GetColor(float health);
}

View File

@@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Game;
using TTT.Game.Commands;
using TTT.Game.lang;
using TTT.Locale;

View File

@@ -3,7 +3,6 @@ using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game;
using TTT.Game.lang;
using TTT.Locale;
using Xunit;

View File

@@ -1,7 +1,6 @@
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Player;
using TTT.Shop;
namespace TTT.Test.Shop;

View File

@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/Content/Site.css" />
<title>&#39;Apache-2.0&#39; reference</title>
</head>
<body>
<div id="main-content">
<h1>Apache License 2.0</h1>
<h2>SPDX identifier</h2>
<div id="license-expression">Apache-2.0</div>
<h2>License text</h2>
<div class="optional-license-text">
<p>Apache License
<br />
Version 2.0, January 2004
<br />
http://www.apache.org/licenses/
</p>
</div>
<div class="optional-license-text">
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
</div>
<ul style="list-style:none">
<li>
<var class="replaceable-license-text"> 1.</var>
Definitions.
<ul style="list-style:none">
<li>
<p>&quot;License&quot; shall mean the terms and conditions for use, reproduction, and distribution
as defined by Sections 1 through 9 of this document.</p>
</li>
<li>
<p>&quot;Licensor&quot; shall mean the copyright owner or entity authorized by the copyright owner
that is granting the License.</p>
</li>
<li>
<p>&quot;Legal Entity&quot; shall mean the union of the acting entity and all other entities that
control, are controlled by, or are under common control with that entity. For the purposes of
this definition, &quot;control&quot; means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or otherwise, or (ii) ownership of
fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such
entity.</p>
</li>
<li>
<p>&quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity exercising
permissions granted by this License.</p>
</li>
<li>
<p>&quot;Source&quot; form shall mean the preferred form for making modifications, including but not
limited to software source code, documentation source, and configuration files.</p>
</li>
<li>
<p>&quot;Object&quot; form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code, generated
documentation, and conversions to other media types.</p>
</li>
<li>
<p>&quot;Work&quot; shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included in or
attached to the work (an example is provided in the Appendix below).</p>
</li>
<li>
<p>&quot;Derivative Works&quot; shall mean any work, whether in Source or Object form, that is based
on (or derived from) the Work and for which the editorial revisions, annotations,
elaborations, or other modifications represent, as a whole, an original work of authorship.
For the purposes of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.</p>
</li>
<li>
<p>&quot;Contribution&quot; shall mean any work of authorship, including the original version of the
Work and any modifications or additions to that Work or Derivative Works thereof, that is
intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an
individual or Legal Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, &quot;submitted&quot; means any form of electronic, verbal, or
written communication sent to the Licensor or its representatives, including but not limited
to communication on electronic mailing lists, source code control systems, and issue tracking
systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and
improving the Work, but excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as &quot;Not a Contribution.&quot;</p>
</li>
<li>
<p>&quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity on behalf of whom
a Contribution has been received by Licensor and subsequently incorporated within the
Work.</p>
</li>
</ul>
</li>
<li>
<var class="replaceable-license-text"> 2.</var>
Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display,
publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or
Object form.
</li>
<li>
<var class="replaceable-license-text"> 3.</var>
Grant of Patent License. Subject to the terms and conditions of this License, each Contributor
hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have made, use, offer
to sell, sell, import, and otherwise transfer the Work, where such license applies only to
those patent claims licensable by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory patent
infringement, then any patent licenses granted to You under this License for that Work shall
terminate as of the date such litigation is filed.
</li>
<li>
<var class="replaceable-license-text"> 4.</var>
Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form, provided that You
meet the following conditions:
<ul style="list-style:none">
<li>
<var class="replaceable-license-text"> (a)</var>
You must give any other recipients of the Work or Derivative Works a copy of this License; and
</li>
<li>
<var class="replaceable-license-text"> (b)</var>
You must cause any modified files to carry prominent notices stating that You changed the files; and
</li>
<li>
<var class="replaceable-license-text"> (c)</var>
You must retain, in the Source form of any Derivative Works that You distribute, all
copyright, patent, trademark, and attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of the Derivative Works; and
</li>
<li>
<var class="replaceable-license-text"> (d)</var>
If the Work includes a &quot;NOTICE&quot; text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the attribution
notices contained within such NOTICE file, excluding those notices that do not pertain to
any part of the Derivative Works, in at least one of the following places: within a NOTICE
text file distributed as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or, within a display generated
by the Derivative Works, if and wherever such third-party notices normally appear. The
contents of the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that You
distribute, alongside or as an addendum to the NOTICE text from the Work, provided that
such additional attribution notices cannot be construed as modifying the License.
</li>
</ul>
<p>You may add Your own copyright statement to Your modifications and may provide additional or
different license terms and conditions for use, reproduction, or distribution of Your
modifications, or for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with the conditions stated
in this License.</p>
</li>
<li>
<var class="replaceable-license-text"> 5.</var>
Submission of Contributions. Unless You explicitly state otherwise, any Contribution
intentionally submitted for inclusion in the Work by You to the Licensor shall be under the
terms and conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate
license agreement you may have executed with Licensor regarding such Contributions.
</li>
<li>
<var class="replaceable-license-text"> 6.</var>
Trademarks. This License does not grant permission to use the trade names, trademarks, service
marks, or product names of the Licensor, except as required for reasonable and customary use
in describing the origin of the Work and reproducing the content of the NOTICE file.
</li>
<li>
<var class="replaceable-license-text"> 7.</var>
Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor
provides the Work (and each Contributor provides its Contributions) on an &quot;AS IS&quot;
BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY,
or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any risks associated with Your
exercise of permissions under this License.
</li>
<li>
<var class="replaceable-license-text"> 8.</var>
Limitation of Liability. In no event and under no legal theory, whether in tort (including
negligence), contract, or otherwise, unless required by applicable law (such as deliberate and
grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for
damages, including any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability to use the Work
(including but not limited to damages for loss of goodwill, work stoppage, computer failure or
malfunction, or any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
</li>
<li>
<var class="replaceable-license-text"> 9.</var>
Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works
thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty,
indemnity, or other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability incurred by, or claims asserted
against, such Contributor by reason of your accepting any such warranty or additional
liability.
</li>
</ul>
<div class="optional-license-text">
<p>END OF TERMS AND CONDITIONS</p>
</div>
<div class="optional-license-text">
<p>APPENDIX: How to apply the Apache License to your work.</p>
<p>To apply the Apache License to your work, attach the following boilerplate notice, with the fields
enclosed by brackets &quot;[]&quot; replaced with your own identifying information. (Don&apos;t
include the brackets!) The text should be enclosed in the appropriate comment syntax for the file
format. We also recommend that a file or class name and description of purpose be included on the same
&quot;printed page&quot; as the copyright notice for easier identification within third-party
archives.</p>
<p>Copyright <var class="replaceable-license-text"> [yyyy] [name of copyright owner]</var></p>
<p>Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
<br />
you may not use this file except in compliance with the License.
<br />
You may obtain a copy of the License at
</p>
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
<p>Unless required by applicable law or agreed to in writing, software
<br />
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
<br />
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
<br />
See the License for the specific language governing permissions and
<br />
limitations under the License.
</p>
</div>
<h2>Standard License Header</h2>
<p>Copyright <var class="replaceable-license-text"> [yyyy] [name of copyright owner]</var></p>
<p>Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
<br />
you may not use this file except in compliance with the License.
<br />
You may obtain a copy of the License at
</p>
<p>http://www.apache.org/licenses/LICENSE-2.0</p>
<p>Unless required by applicable law or agreed to in writing, software
<br />
distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
<br />
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
<br />
See the License for the specific language governing permissions and
<br />
limitations under the License.
</p>
<h2>Notes</h2>
<p>This license was released January 2004</p>
<h2>SPDX web page</h2>
<ul>
<li><a href="https://spdx.org/licenses/Apache-2.0.html">https://spdx.org/licenses/Apache-2.0.html</a></li>
</ul>
<h2>Notice</h2>
<p>This license content is provided by the <a href="https://spdx.dev/">SPDX project</a>. For more information about <b>licenses.nuget.org</b>, see <a href="https://aka.ms/licenses.nuget.org">our documentation</a>.
<p><i>Data pulled from <a href="https://github.com/spdx/license-list-data">spdx/license-list-data</a> on November 6, 2024.</i></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/Content/Site.css" />
<title>&#39;MIT&#39; reference</title>
</head>
<body>
<div id="main-content">
<h1>MIT License</h1>
<h2>SPDX identifier</h2>
<div id="license-expression">MIT</div>
<h2>License text</h2>
<div class="optional-license-text">
<p>MIT License</p>
</div>
<div class="replaceable-license-text">
<p>Copyright (c) &lt;year&gt; &lt;copyright holders&gt;
</p>
</div>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of <var class="replaceable-license-text"> this software and
associated documentation files</var> (the &quot;Software&quot;), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:</p>
<p>The above copyright notice and this permission notice
<var class="optional-license-text"> (including the next paragraph)</var>
shall be included in all copies or substantial
portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL <var class="replaceable-license-text"> THE AUTHORS OR COPYRIGHT HOLDERS</var> BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h2>SPDX web page</h2>
<ul>
<li><a href="https://spdx.org/licenses/MIT.html">https://spdx.org/licenses/MIT.html</a></li>
</ul>
<h2>Notice</h2>
<p>This license content is provided by the <a href="https://spdx.dev/">SPDX project</a>. For more information about <b>licenses.nuget.org</b>, see <a href="https://aka.ms/licenses.nuget.org">our documentation</a>.
<p><i>Data pulled from <a href="https://github.com/spdx/license-list-data">spdx/license-list-data</a> on November 6, 2024.</i></p>
</div>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/Content/Site.css" />
<title>&#39;MIT&#39; reference</title>
</head>
<body>
<div id="main-content">
<h1>MIT License</h1>
<h2>SPDX identifier</h2>
<div id="license-expression">MIT</div>
<h2>License text</h2>
<div class="optional-license-text">
<p>MIT License</p>
</div>
<div class="replaceable-license-text">
<p>Copyright (c) &lt;year&gt; &lt;copyright holders&gt;
</p>
</div>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of <var class="replaceable-license-text"> this software and
associated documentation files</var> (the &quot;Software&quot;), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:</p>
<p>The above copyright notice and this permission notice
<var class="optional-license-text"> (including the next paragraph)</var>
shall be included in all copies or substantial
portions of the Software.</p>
<p>THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL <var class="replaceable-license-text"> THE AUTHORS OR COPYRIGHT HOLDERS</var> BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
<h2>SPDX web page</h2>
<ul>
<li><a href="https://spdx.org/licenses/MIT.html">https://spdx.org/licenses/MIT.html</a></li>
</ul>
<h2>Notice</h2>
<p>This license content is provided by the <a href="https://spdx.dev/">SPDX project</a>. For more information about <b>licenses.nuget.org</b>, see <a href="https://aka.ms/licenses.nuget.org">our documentation</a>.
<p><i>Data pulled from <a href="https://github.com/spdx/license-list-data">spdx/license-list-data</a> on November 6, 2024.</i></p>
</div>
</body>
</html>