Compare commits

...

28 Commits

Author SHA1 Message Date
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
MSWS
ddf52f057d Update from main 2025-10-01 21:50:19 -07:00
Isaac
c2ecba1847 Merge branch 'main' into dev 2025-10-01 20:39:01 -07:00
Isaac
354ccf2fbe Update TTT/Shop/Items/Detective/Stickers/Stickers.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Isaac <git@msws.xyz>
2025-10-01 20:38:03 -07:00
Isaac
9d1a7f5618 feat: Integrate ShopAPI and add station item features +semver:minor (resolves #84) (#92)
```
- Integrate ShopAPI into various components including PlayerPurchaseItemEvent, DnaScanner, and multiple stations for improved shop functionalities.
- Introduce new HealthStationConfig and DamageStationConfig for configuring health and damage-related station items.
- Update CS2 project to target .NET 8.0 and enable advanced C# features, while removing outdated folder references for streamlined project structure.
- Correct namespace declarations across multiple files, enhancing consistency and organizational clarity within the codebase.
- Add new or updated commands and extensions, such as SetHealthCommand, to improve player health management and interaction.
- Enhance localization features by importing TTT.Game.lang across various roles, commands, and logging implementations.
- Incorporate new logic in DNA scanning and station interactions, including damage dealing and healing over time based on player proximity.
```
2025-10-01 19:13:36 -07:00
MSWS
ea62b312be Update unit tests 2025-10-01 19:12:10 -07:00
MSWS
6778531312 feat: Integrate ShopAPI and add station item features +semver:minor
```
- Integrate ShopAPI into various components including PlayerPurchaseItemEvent, DnaScanner, and multiple stations for improved shop functionalities.
- Introduce new HealthStationConfig and DamageStationConfig for configuring health and damage-related station items.
- Update CS2 project to target .NET 8.0 and enable advanced C# features, while removing outdated folder references for streamlined project structure.
- Correct namespace declarations across multiple files, enhancing consistency and organizational clarity within the codebase.
- Add new or updated commands and extensions, such as SetHealthCommand, to improve player health management and interaction.
- Enhance localization features by importing TTT.Game.lang across various roles, commands, and logging implementations.
- Incorporate new logic in DNA scanning and station interactions, including damage dealing and healing over time based on player proximity.
```
2025-10-01 19:08:30 -07:00
MSWS
8ab4328d9b Fix YML 2025-10-01 15:01:35 -07:00
MSWS
c5a91f334d refactor: Improve item not found handling and code readability
- Add a new message for item not found in `en.yml` to enhance user error feedback
- Clean up `GiveItemCommand.cs` by importing `TTT.Locale` and removing debug messages
- Improve `CS2AliveSpoofer.cs` readability with better conditional logic and early return handling
- Enhance `BuyCommand.cs` by removing redundant searches and utilizing localization for consistency
- Change `DecayTime` in `DnaScannerConfig.cs` from 3 minutes to 10 seconds for faster testing
2025-10-01 14:55:28 -07:00
MSWS
b2f4474e8f Reformat 2025-10-01 14:39:15 -07:00
Isaac
8aee59a87e Feat/dna (resolves #87) (#91) 2025-10-01 14:21:04 -07:00
MSWS
e27bddf8e2 feat: Implement enhanced DNA scanner messaging +semver:minor
- Clean up `ShopAPI.csproj` by removing redundant project references.
- Remove the DNA Scanner item and its description from the English language file in `Shop`.
- Streamline `DnaScanner.cs` by eliminating an unnecessary using directive.
- Enhance `DnaListener.cs` to improve messaging logic with role information and implement new message templating for DNA scans.
- Refactor `DnaMsgs.cs` by adjusting namespaces and adding message properties for enhanced DNA scan functionalities.
- Update `CS2.csproj` by removing a duplicate project reference.
- Expand `en.yml` in `CS2` with translations for DNA scanner items and messages for DNA scanning, including specific scenarios involving suicide.
2025-10-01 14:17:27 -07:00
MSWS
4514e9baa0 feat: Introduce DNA scanner functionality
- Extend functionality in `DnaListener.cs` by adding dependency injection and enhancing the `OnPropPickup` event handler with player and body checks.
- Add `TimeOfDeath` property to `CS2Body.cs` for improved tracking of body creation time.
- Introduce `DnaScannerServiceCollection` in `DnaScanner.cs` for better service management and mod behavior registration.
- Extend `IBody` interface with `TimeOfDeath` to track deceased player identification time.
- Update `ShopServiceCollection.cs` to integrate `AddDnaScannerServices`, enhancing shop capabilities with DNA scanner features.
2025-10-01 11:23:31 -07:00
MSWS
67755c36c6 Tweak AI prompt and input 2025-09-30 18:25:18 -07:00
MSWS
eaf1ab627e Begin work on adding CS2-specific items
```
- Rename and reorganize directories for DNA-related items to new path under "CS2/Items/DNA"
- Relocate RoleRestrictedItem.cs while maintaining its original functionality
- Move and update DnaListener.cs with a low-priority event handler and add an execution order comment
- Rename BaseItem.cs file path as part of project structure reorganization
- Update CS2.csproj by adding "Items\" folder and correcting duplicate project reference
```
2025-09-30 18:22:13 -07:00
MSWS
57bef00055 Refactor Shop api into its own project, separate from impl 2025-09-30 18:18:07 -07:00
MSWS
7dd6d4dd38 Finalize stickers (resolves #89) 2025-09-30 17:46:52 -07:00
Isaac
2e6743c25d Miscelleaneous Tweaks
<p dir="auto">This pull request appears to be a development branch merge
 that implements several enhancements and fixes to the TTT (Trouble in 
Terrorist Town) game system. The changes focus on improving weapon 
handling, adding new test coverage, and enhancing player visual
effects.</p>
<ul dir="auto">
<li>Refactors weapon API by renaming <code class="notranslate">Id</code>
property to <code class="notranslate">WeaponId</code> for better
clarity</li>
<li>Implements comprehensive shop and weapon testing infrastructure</li>
<li>Adds screen color effects and player visual enhancements</li>
</ul>
<h3 dir="auto">Reviewed Changes</h3>
<p dir="auto">Copilot reviewed 32 out of 32 changed files in this pull
request and generated 2 comments.</p>
<details open="">
<summary>Show a summary per file</summary>
<markdown-accessiblity-table data-catalyst="">
File | Description
-- | --
TTT/API/IWeapon.cs | Renames weapon identifier property from Id to
WeaponId
TTT/Test/Shop/ShopTests.cs | Adds comprehensive test coverage for shop
functionality
TTT/Test/Shop/Items/DeagleTests.cs | Implements tests for one-shot
deagle weapon behavior
TTT/Test/TestPlayer.cs | Enhances test player with computed IsAlive
property
TTT/CS2/Player/CS2InventoryManager.cs | Adds weapon slot management and
refactors weapon handling
TTT/CS2/Extensions/PlayerExtensions.cs | Implements screen color fade
effects for players
TTT/CS2/Listeners/ScreenColorApplier.cs | Adds role-based screen color
feedback
TTT/Game/Roles/BaseWeapon.cs | Updates weapon class to use WeaponId
property
TTT/Shop/Items/OneShotDeagle/OneShotDeagle.cs | Updates deagle
implementation for new weapon API

</markdown-accessiblity-table></details>
2025-09-28 01:32:07 -07:00
109 changed files with 1840 additions and 287 deletions

View File

@@ -112,14 +112,14 @@ jobs:
curr="${{ steps.gitversion.outputs.fullSemVer }}"
# Choose what you want in the raw feed: %s = subject only, %B = full message
GIT_LOG_FORMAT='%s'
GIT_LOG_FORMAT='%B'
if [[ "$prev" == "0.0.0" ]]; then
# First release: whole history to this tag, first-parent to reflect mains narrative
git log --first-parent --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$curr" > CHANGELOG.md
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$curr" > CHANGELOG.md
else
# Strict range between the previous reachable tag and the new tag on this lineage
git log --first-parent --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$prev..$curr" > CHANGELOG.md
git log --no-merges --format="${GIT_LOG_FORMAT}" --reverse "$prev..$curr" > CHANGELOG.md
fi
# Fallback in case nothing was captured
@@ -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 editor. Rewrite the text so it is grouped by Features, Fixes, Docs, and Chore. Use clear user-facing language. Remove internal ticket IDs and commit hashes unless essential. Merge duplicates. Use imperative voice. Output valid Markdown only. Include a short summary at the top." \
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." \
--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

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

@@ -11,7 +11,7 @@
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.332"/>
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="8.0.3"/>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
<PackageReference Include="YamlDotNet" Version="16.3.0"/>
</ItemGroup>

View File

@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shop", "TTT\Shop\Shop.cspro
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Karma", "TTT\Karma\Karma.csproj", "{AFC791EC-750C-423F-9F35-87636657E990}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ShopAPI", "TTT\ShopAPI\ShopAPI.csproj", "{16F720B5-9D45-47BF-8C80-4F91005E36D1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -66,6 +68,10 @@ Global
{AFC791EC-750C-423F-9F35-87636657E990}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AFC791EC-750C-423F-9F35-87636657E990}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AFC791EC-750C-423F-9F35-87636657E990}.Release|Any CPU.Build.0 = Release|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16F720B5-9D45-47BF-8C80-4F91005E36D1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection

View File

@@ -6,6 +6,6 @@
<RootNamespace>TTT.API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="System.Text.Json" Version="8.0.5"/>
</ItemGroup>
</Project>

View File

@@ -12,20 +12,17 @@ public static class ServiceCollectionExtensions {
public static void AddModBehavior<TExtension>(
this IServiceCollection collection)
where TExtension : class, ITerrorModule {
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule))) {
if (typeof(TExtension).IsAssignableTo(typeof(IPluginModule)))
collection.AddTransient<IPluginModule>(provider
=> (provider.GetRequiredService<TExtension>() as IPluginModule)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(IListener))) {
if (typeof(TExtension).IsAssignableTo(typeof(IListener)))
collection.AddTransient<IListener>(provider
=> (provider.GetRequiredService<TExtension>() as IListener)!);
}
if (typeof(TExtension).IsAssignableTo(typeof(ICommand))) {
if (typeof(TExtension).IsAssignableTo(typeof(ICommand)))
collection.AddTransient<ICommand>(provider
=> (provider.GetRequiredService<TExtension>() as ICommand)!);
}
collection.AddScoped<TExtension>();

View File

@@ -1,9 +1,9 @@
namespace TTT.API.Player;
/// <summary>
/// Assumes a maximum of 64 players.
/// Each bit in the bitmask represents whether a player is visible to the client.
/// Bit 0 is unused, bit 1 represents player 1, bit 2 represents player 2, and so on.
/// Assumes a maximum of 64 players.
/// Each bit in the bitmask represents whether a player is visible to the client.
/// Bit 0 is unused, bit 1 represents player 1, bit 2 represents player 2, and so on.
/// </summary>
public interface IIconManager {
ulong GetVisiblePlayers(int client);

View File

@@ -12,7 +12,7 @@
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\Karma\Karma.csproj"/>
<ProjectReference Include="..\Shop\Shop.csproj"/>
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@@ -1,36 +1,19 @@
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.Game.Roles;
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;
public CS2Body WithWeapon(IWeapon weapon) {
MurderWeapon = weapon;

View File

@@ -1,5 +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;
@@ -20,8 +22,6 @@ using TTT.CS2.Listeners;
using TTT.CS2.Player;
using TTT.Game;
using TTT.Locale;
using TTT.Shop;
using TTT.Shop.Items;
namespace TTT.CS2;
@@ -40,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,8 +5,8 @@ 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;
namespace TTT.CS2.Command;

View File

@@ -1,12 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;
using TTT.Shop;
namespace TTT.CS2.Command.Test;
public class GiveItemCommand(IServiceProvider provider) : ICommand {
private readonly IShop shop = provider.GetRequiredService<IShop>();
public void Dispose() { }
public void Start() { }
@@ -19,8 +20,7 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
if (info.ArgCount == 1) return Task.FromResult(CommandResult.PRINT_USAGE);
var query = string.Join(" ", info.Args.Skip(1));
info.ReplySync($"Searching for item: {query}");
var item = searchItem(query);
var item = searchItem(query);
if (item == null) {
info.ReplySync($"Item '{query}' not found.");
return Task.FromResult(CommandResult.ERROR);

View File

@@ -0,0 +1,27 @@
using TTT.API.Command;
using TTT.API.Player;
namespace TTT.CS2.Command.Test;
public class SetHealthCommand : ICommand {
public string Id => "sethealth";
public void Dispose() { }
public void Start() { }
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
if (info.ArgCount != 2) return Task.FromResult(CommandResult.PRINT_USAGE);
if (!int.TryParse(info.Args[1], out var health)) {
info.ReplySync("Invalid health value.");
return Task.FromResult(CommandResult.ERROR);
}
executor.Health = health;
info.ReplySync($"Set health of {executor.Name} to {health}.");
return Task.FromResult(CommandResult.SUCCESS);
}
}

View File

@@ -2,7 +2,6 @@
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Command;
using TTT.API.Player;
using TTT.CS2.API;
namespace TTT.CS2.Command.Test;

View File

@@ -23,6 +23,7 @@ public class TestCommand(IServiceProvider provider) : ICommand, IPluginModule {
subCommands.Add("giveitem", new GiveItemCommand(provider));
subCommands.Add("index", new IndexCommand(provider));
subCommands.Add("showicons", new ShowIconsCommand(provider));
subCommands.Add("sethealth", new SetHealthCommand());
}
public Task<CommandResult>

View File

@@ -7,7 +7,7 @@ using TTT.API.Storage;
using TTT.CS2.Validators;
using TTT.Game;
namespace TTT.CS2.Game;
namespace TTT.CS2.Configs;
public class CS2GameConfig : IStorage<TTTConfig>, IPluginModule {
public static readonly FakeConVar<int> CV_ROUND_COUNTDOWN = new(

View File

@@ -2,9 +2,9 @@
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.Shop;
namespace TTT.CS2.Configs;

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

@@ -2,10 +2,10 @@
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;
using TTT.Shop.Items;
namespace TTT.CS2.Configs.ShopItems;
@@ -32,17 +32,14 @@ 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 {
Price = CV_PRICE.Value,
DoesFriendlyFire = CV_FRIENDLY_FIRE.Value,
Weapon = CV_WEAPON.Value,
KillShooterOnFF = CV_KILL_SHOOTER_ON_FF.Value,
KillShooterOnFF = CV_KILL_SHOOTER_ON_FF.Value
};
return Task.FromResult<OneShotDeagleConfig?>(cfg);

View File

@@ -6,6 +6,10 @@ using CounterStrikeSharp.API.Modules.UserMessages;
namespace TTT.CS2.Extensions;
public static class PlayerExtensions {
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
public static CBasePlayerWeapon? GetWeaponBase(
this CCSPlayerController player, string designerName) {
if (!player.IsValid) return null;
@@ -26,6 +30,26 @@ public static class PlayerExtensions {
ev.FireEvent(false);
}
public static void SetHealth(this CCSPlayerController player, int health) {
if (player.Pawn.Value == null) return;
if (health <= 0) {
player.CommitSuicide(false, true);
return;
}
player.Pawn.Value.Health = health;
Utilities.SetStateChanged(player.Pawn.Value, "CBaseEntity", "m_iHealth");
}
public static int GetHealth(this CCSPlayerController player) {
return player.Pawn.Value?.Health ?? 0;
}
public static void AddHealth(this CCSPlayerController player, int health) {
if (player.Pawn.Value == null) return;
player.SetHealth(player.Pawn.Value.Health + health);
}
public static void SetColor(this CCSPlayerController player, Color color) {
if (!player.IsValid) return;
var pawn = player.Pawn.Value;
@@ -37,10 +61,6 @@ public static class PlayerExtensions {
pawn.SetColor(color);
}
public enum FadeFlags {
FADE_IN, FADE_OUT, FADE_STAYOUT
}
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) {

View File

@@ -6,6 +6,7 @@ using TTT.API.Role;
using TTT.CS2.Roles;
using TTT.CS2.Utils;
using TTT.Game;
using TTT.Game.lang;
using TTT.Game.Roles;
namespace TTT.CS2.Game;

View File

@@ -11,7 +11,7 @@ public class CS2GameManager(IServiceProvider provider) : GameManager(provider) {
provider.GetRequiredService<IMessenger>();
public override IGame CreateGame() {
messenger.Debug($"Attempting to create a new CS2 game...");
messenger.Debug("Attempting to create a new CS2 game...");
switch (ActiveGame) {
case { State: State.IN_PROGRESS or State.COUNTDOWN }:
throw new InvalidOperationException(

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

@@ -3,8 +3,6 @@ using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using TTT.API;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.Events.Player;

View File

@@ -9,7 +9,6 @@ using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.CS2.API;
using TTT.CS2.Extensions;
using TTT.CS2.Hats;
using TTT.CS2.Roles;
@@ -28,20 +27,53 @@ 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];
private readonly IPlayerConverter<CCSPlayerController> players =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly ITextSpawner? textSpawner =
provider.GetService<ITextSpawner>();
private readonly HashSet<int> traitorsThisRound = new();
private readonly ulong[] visibilities = new ulong[64];
private HashSet<int> traitorsThisRound = new();
public ulong GetVisiblePlayers(int client) {
if (client < 1 || client >= visibilities.Length)
throw new ArgumentOutOfRangeException(nameof(client));
return visibilities[client];
}
// private readonly IDictionary<int, IEnumerable<CPointWorldText>> icons =
// new Dictionary<int, IEnumerable<CPointWorldText>>();
private readonly IEnumerable<CPointWorldText>?[] icons =
new IEnumerable<CPointWorldText>[64];
public void SetVisiblePlayers(int client, ulong playersBitmask) {
guardRange(client, nameof(client));
visibilities[client] = playersBitmask;
}
public void RevealToAll(int client) {
guardRange(client, nameof(client));
for (var i = 0; i < visibilities.Length; i++)
visibilities[i] |= 1UL << client;
}
public void AddVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] |= 1UL << player;
}
public void RemoveVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] &= ~(1UL << player);
}
public void ClearAllVisibility() {
Array.Clear(visibilities, 0, visibilities.Length);
}
public void Start(BasePlugin? plugin, bool hotReload) {
plugin
@@ -157,41 +189,8 @@ public class RoleIconsHandler(IServiceProvider provider)
}
}
public ulong GetVisiblePlayers(int client) {
if (client < 1 || client >= visibilities.Length)
throw new ArgumentOutOfRangeException(nameof(client));
return visibilities[client];
}
public void SetVisiblePlayers(int client, ulong playersBitmask) {
guardRange(client, nameof(client));
visibilities[client] = playersBitmask;
}
public void RevealToAll(int client) {
guardRange(client, nameof(client));
for (var i = 0; i < visibilities.Length; i++)
visibilities[i] |= 1UL << client;
}
public void AddVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] |= 1UL << player;
}
public void RemoveVisiblePlayer(int client, int player) {
guardRange(client, nameof(client));
guardRange(player, nameof(player));
visibilities[client] &= ~(1UL << player);
}
private void guardRange(int index, string name) {
if (index < 0 || index >= visibilities.Length)
throw new ArgumentOutOfRangeException(name);
}
public void ClearAllVisibility() {
Array.Clear(visibilities, 0, visibilities.Length);
}
}

View File

@@ -19,7 +19,7 @@ public interface ITextSpawner {
IEnumerable<CPointWorldText> CreateTextHat(TextSetting setting,
CCSPlayerController player);
IEnumerable<CPointWorldText> CreateTextScreen(TextSetting setting,
CCSPlayerController player);
}

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

@@ -0,0 +1,94 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.API;
using TTT.CS2.Events;
using TTT.Game.Events.Game;
using TTT.Game.Listeners;
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>();
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
private readonly Dictionary<string, DateTime> lastMessages = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
// Low priority to allow body identification to happen first
[UsedImplicitly]
[EventHandler(Priority = Priority.LOW)]
public void OnPropPickup(PropPickupEvent ev) {
if (ev.Player is not IOnlinePlayer player) return;
if (!shop.HasItem<DnaScanner>(player)) return;
if (!bodies.TryLookup(ev.Prop.Index.ToString(), out var body)) return;
if (body == null) return;
var victimRole = Roles.GetRoles(body.OfPlayer).FirstOrDefault();
if (victimRole == null) return;
if (lastMessages.TryGetValue(player.Id + "." + body.Id,
out var lastMessageTime))
if (DateTime.Now - lastMessageTime < cooldown)
return;
lastMessages[player.Id + "." + body.Id] = DateTime.Now;
if (DateTime.Now - body.TimeOfDeath > config.DecayTime) {
Messenger.Message(player,
Locale[DnaMsgs.SHOP_ITEM_DNA_EXPIRED(victimRole, body.OfPlayer)]);
return;
}
if (body.Killer == null) {
var explanation =
missingDnaExplanations[
Random.Shared.Next(missingDnaExplanations.Length)];
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED_OTHER(victimRole, body.OfPlayer,
explanation)]);
return;
}
if (body.Killer == body.OfPlayer) {
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED_SUICIDE(victimRole, body.OfPlayer)]);
return;
}
Messenger.Message(player,
Locale[
DnaMsgs.SHOP_ITEM_DNA_SCANNED(victimRole, body.OfPlayer, body.Killer)]);
}
[EventHandler]
public void OnRoundEnd(GameStateUpdateEvent ev) {
if (ev.NewState != State.FINISHED) return;
lastMessages.Clear();
}
}

View File

@@ -0,0 +1,36 @@
using TTT.API.Player;
using TTT.API.Role;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.CS2.Items.DNA;
public class DnaMsgs {
public static IMsg SHOP_ITEM_DNA => MsgFactory.Create(nameof(SHOP_ITEM_DNA));
public static IMsg SHOP_ITEM_DNA_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_DNA_DESC));
public static IMsg SHOP_ITEM_DNA_SCANNED(IRole victimRole, IPlayer player,
IPlayer killer) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_SCANNED),
GameMsgs.GetRolePrefix(victimRole), player.Name, killer.Name);
}
public static IMsg SHOP_ITEM_DNA_SCANNED_OTHER(IRole victimRole,
IPlayer player, string explanation) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_SCANNED_OTHER),
GameMsgs.GetRolePrefix(victimRole), player.Name);
}
public static IMsg
SHOP_ITEM_DNA_SCANNED_SUICIDE(IRole victimRole, IPlayer player) {
return SHOP_ITEM_DNA_SCANNED_OTHER(victimRole, player,
"they killed themselves");
}
public static IMsg SHOP_ITEM_DNA_EXPIRED(IRole victimRole, IPlayer player) {
return MsgFactory.Create(nameof(SHOP_ITEM_DNA_EXPIRED),
GameMsgs.GetRolePrefix(victimRole), player.Name);
}
}

View File

@@ -0,0 +1,36 @@
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.CS2.Items.DNA;
public static class DnaScannerServiceCollection {
public static void AddDnaScannerServices(this IServiceCollection collection) {
collection.AddModBehavior<DnaScanner>();
collection.AddModBehavior<DnaListener>();
}
}
public class DnaScanner(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly DnaScannerConfig config = provider
.GetService<IStorage<DnaScannerConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DnaScannerConfig();
public override string Name => Locale[DnaMsgs.SHOP_ITEM_DNA];
public override string Description => Locale[DnaMsgs.SHOP_ITEM_DNA_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
return base.CanPurchase(player);
}
}

View File

@@ -0,0 +1,74 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
namespace TTT.CS2.Items.Station;
public static class DamageStationCollection {
public static void AddDamageStation(this IServiceCollection collection) {
collection.AddModBehavior<DamageStation>();
}
}
public class DamageStation(IServiceProvider provider) : StationItem(provider,
provider.GetService<IStorage<DamageStationConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()) {
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) {
if (Math.Abs(info.HealthGiven) > Math.Abs(_Config.TotalHealthGiven)) {
props.Remove(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerMapping = players.Select(p
=> (ApiPlayer: p, GamePlayer: converter.GetPlayer(p)))
.Where(m => m.GamePlayer != null);
var playerDists = playerMapping
.Where(t => !roles.GetRoles(t.ApiPlayer).OfType<TraitorRole>().Any())
.Select(t => (t.ApiPlayer, Origin: t.GamePlayer!.Pawn.Value?.AbsOrigin,
t.GamePlayer))
.Where(t => t is { Origin: not null, ApiPlayer.IsAlive: true })
.Select(t
=> (t.ApiPlayer, Dist: t.Origin!.Distance(propPos), t.GamePlayer))
.Where(t => t.Dist <= _Config.MaxRange)
.ToList();
foreach (var (player, dist, gamePlayer) in playerDists) {
var healthScale = 1.0 - dist / _Config.MaxRange;
var damageAmount =
(int)Math.Floor(_Config.HealthIncrements * healthScale);
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}
}

View File

@@ -0,0 +1,57 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Storage;
using TTT.CS2.Extensions;
namespace TTT.CS2.Items.Station;
public static class HealthStationCollection {
public static void AddHealthStation(this IServiceCollection collection) {
collection.AddModBehavior<HealthStation>();
}
}
public class HealthStation(IServiceProvider provider) : StationItem(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();
foreach (var (prop, info) in props) {
if (Math.Abs(info.HealthGiven) > _Config.TotalHealthGiven) {
props.Remove(prop);
continue;
}
var propPos = prop.AbsOrigin;
if (propPos == null) continue;
var playerDists = players
.Select(p => (Player: p, Pos: p.Pawn.Value?.AbsOrigin))
.Where(t => t is { Pos: not null, Player.Pawn.Value.Health: > 0 })
.Select(t => (t.Player, Dist: t.Pos!.Distance(propPos)))
.Where(t => t.Dist <= _Config.MaxRange)
.ToList();
foreach (var (player, dist) in playerDists) {
var maxHp = player.Pawn.Value?.MaxHealth ?? 100;
var healthScale = 1.0 - dist / _Config.MaxRange;
var healAmount =
(int)Math.Ceiling(_Config.HealthIncrements * healthScale);
var newHealth = Math.Min(player.GetHealth() + healAmount, maxHp);
player.SetHealth(newHealth);
info.HealthGiven += healAmount;
player.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}
}

View File

@@ -0,0 +1,9 @@
using CounterStrikeSharp.API.Core;
namespace TTT.CS2.Items.Station;
public class StationInfo(CPhysicsPropMultiplayer prop, int health) {
public readonly CPhysicsPropMultiplayer Prop = prop;
public int Health = health;
public int HealthGiven = 0;
}

View File

@@ -0,0 +1,146 @@
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;
using TTT.API;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.CS2.Extensions;
using TTT.Game.Roles;
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;
protected readonly IPlayerConverter<CCSPlayerController> Converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IMessenger messenger =
provider.GetRequiredService<IMessenger>();
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,
() => Server.NextWorldUpdate(onInterval));
}
public void Start(BasePlugin? plugin) {
Start();
messenger.DebugAnnounce("Starting StationItem2 ");
plugin
?.RegisterListener<
CounterStrikeSharp.API.Core.Listeners.OnServerPrecacheResources>(m => {
m.AddResource("models/props/cs_office/microwave.vmdl");
});
}
public override void Dispose() {
base.Dispose();
intervalHandle?.Dispose();
}
[UsedImplicitly]
[GameEventHandler]
public HookResult OnBulletImpact(EventBulletImpact ev, GameEventInfo info) {
var hitVec = new Vector(ev.X, ev.Y, ev.Z);
var nearest = props
.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)
.FirstOrDefault();
if (nearest.Key == null || nearest.Value == null
|| nearest.Distance > PROP_SIZE_SQUARED)
return HookResult.Continue;
var dmg = getBulletDamage(ev);
nearest.Value.Health -= dmg;
if (nearest.Value.Health <= 0) {
nearest.Key.AcceptInput("Kill");
props.Remove(nearest.Key);
return HookResult.Continue;
}
nearest.Key.SetColor(
_Config.GetColor(nearest.Value.Health / (float)_Config.StationHealth));
return HookResult.Continue;
}
private int getBulletDamage(EventBulletImpact ev) {
var user = ev.Userid;
var weapon = user?.Pawn.Value?.WeaponServices?.ActiveWeapon.Value;
var dist =
(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 ?? "");
var total = (int)(baseDamage * distScale);
return Math.Max(total, 1);
}
private int getBaseDamage(string designerWeapon) {
return designerWeapon switch {
"weapon_awp" => 115,
"weapon_glock" => 8,
"weapon_usp_silencer" => 20,
"weapon_deagle" => 40,
_ when Tag.PISTOLS.Contains(designerWeapon) => 10,
_ when Tag.SMGS.Contains(designerWeapon) => 15,
_ when Tag.SHOTGUNS.Contains(designerWeapon) => 25,
_ when Tag.RIFLES.Contains(designerWeapon) => 45,
_ => 5
};
}
public override void OnPurchase(IOnlinePlayer player) {
Server.NextWorldUpdate(() => {
var prop =
Utilities.CreateEntityByName<CPhysicsPropMultiplayer>(
"prop_physics_multiplayer");
if (prop == null) return;
props[prop] = new StationInfo(prop, _Config.StationHealth);
prop.SetModel("models/props/cs_office/microwave.vmdl");
prop.DispatchSpawn();
var gamePlayer = Converter.GetPlayer(player);
if (gamePlayer == null || !gamePlayer.Pawn.IsValid
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
prop.Teleport(spawnPos);
});
}
abstract protected void onInterval();
}

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,8 +7,8 @@ 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;
using TTT.Game.Roles;

View File

@@ -22,7 +22,6 @@ public class ScreenColorApplier(IServiceProvider provider)
var player = converter.GetPlayer(ev.Player);
var alphaColor = Color.FromArgb(64, ev.Role.Color);
if (player != null)
player.ColorScreen(alphaColor, 3, 1,
flags: PlayerExtensions.FadeFlags.FADE_OUT);
player.ColorScreen(alphaColor, 3, 1, PlayerExtensions.FadeFlags.FADE_OUT);
}
}

View File

@@ -25,6 +25,8 @@ public class CS2AliveSpoofer : IAliveSpoofer, IPluginModule {
"m_bPawnIsAlive");
});
});
return;
}
FakeAlivePlayers.Add(player);

View File

@@ -18,71 +18,6 @@ public class CS2InventoryManager(
});
}
private void giveWeapon(CCSPlayerController player, IWeapon weapon) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null) {
if (weapon.WeaponId.Equals("weapon_revolver")) {
weaponBase = player.GetWeaponBase("weapon_deagle");
}
}
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null)
weaponBase.ReserveAmmo[0] = weapon.ReserveAmmo.Value;
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon", "m_iClip1");
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon",
"m_pReserveAmmo");
}
public static gear_slot_t IntToSlot(int slot)
=> slot switch {
0 => gear_slot_t.GEAR_SLOT_RIFLE,
1 => gear_slot_t.GEAR_SLOT_PISTOL,
2 => gear_slot_t.GEAR_SLOT_KNIFE,
3 => gear_slot_t.GEAR_SLOT_UTILITY,
4 => gear_slot_t.GEAR_SLOT_C4,
_ => gear_slot_t.GEAR_SLOT_FIRST
};
public static int SlotToInt(gear_slot_t slot)
=> slot switch {
gear_slot_t.GEAR_SLOT_RIFLE => 0,
gear_slot_t.GEAR_SLOT_PISTOL => 1,
gear_slot_t.GEAR_SLOT_KNIFE => 2,
gear_slot_t.GEAR_SLOT_UTILITY => 3,
gear_slot_t.GEAR_SLOT_C4 => 4,
_ => -1
};
private void clearSlot(CCSPlayerController player,
params gear_slot_t[] slots) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
var weapons = player.Pawn.Value?.WeaponServices?.MyWeapons;
if (weapons == null || weapons.Count == 0) return;
foreach (var weapon in weapons) {
if (!weapon.IsValid || weapon.Value == null) continue;
if (!weapon.Value.IsValid
|| !weapon.Value.DesignerName.StartsWith("weapon_"))
continue;
if (weapon.Value.Entity == null) continue;
var weaponBase = weapon.Value.As<CCSWeaponBase>();
if (!weaponBase.IsValid || (weaponBase.Entity == null)) continue;
var vdata = weaponBase.VData;
if (vdata == null) continue;
if (!slots.Contains(vdata.GearSlot)) continue;
weapon.Value.AddEntityIOEvent("Kill", weapon.Value);
}
}
public Task RemoveWeapon(IOnlinePlayer player, string weaponId) {
return Server.NextWorldUpdateAsync(() => {
if (!player.IsAlive) return;
@@ -131,4 +66,69 @@ public class CS2InventoryManager(
gamePlayer.RemoveWeapons();
});
}
private void giveWeapon(CCSPlayerController player, IWeapon weapon) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
// Give the weapon
player.GiveNamedItem(weapon.WeaponId);
// Set ammo if applicable
var weaponBase = player.GetWeaponBase(weapon.WeaponId);
if (weaponBase == null)
if (weapon.WeaponId.Equals("weapon_revolver"))
weaponBase = player.GetWeaponBase("weapon_deagle");
if (weaponBase == null) return;
if (weapon.CurrentAmmo != null) weaponBase.Clip1 = weapon.CurrentAmmo.Value;
if (weapon.ReserveAmmo != null)
weaponBase.ReserveAmmo[0] = weapon.ReserveAmmo.Value;
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon", "m_iClip1");
Utilities.SetStateChanged(weaponBase, "CBasePlayerWeapon",
"m_pReserveAmmo");
}
public static gear_slot_t IntToSlot(int slot) {
return slot switch {
0 => gear_slot_t.GEAR_SLOT_RIFLE,
1 => gear_slot_t.GEAR_SLOT_PISTOL,
2 => gear_slot_t.GEAR_SLOT_KNIFE,
3 => gear_slot_t.GEAR_SLOT_UTILITY,
4 => gear_slot_t.GEAR_SLOT_C4,
_ => gear_slot_t.GEAR_SLOT_FIRST
};
}
public static int SlotToInt(gear_slot_t slot) {
return slot switch {
gear_slot_t.GEAR_SLOT_RIFLE => 0,
gear_slot_t.GEAR_SLOT_PISTOL => 1,
gear_slot_t.GEAR_SLOT_KNIFE => 2,
gear_slot_t.GEAR_SLOT_UTILITY => 3,
gear_slot_t.GEAR_SLOT_C4 => 4,
_ => -1
};
}
private void clearSlot(CCSPlayerController player,
params gear_slot_t[] slots) {
if (player.Team is CsTeam.None or CsTeam.Spectator) return;
var weapons = player.Pawn.Value?.WeaponServices?.MyWeapons;
if (weapons == null || weapons.Count == 0) return;
foreach (var weapon in weapons) {
if (!weapon.IsValid || weapon.Value == null) continue;
if (!weapon.Value.IsValid
|| !weapon.Value.DesignerName.StartsWith("weapon_"))
continue;
if (weapon.Value.Entity == null) continue;
var weaponBase = weapon.Value.As<CCSWeaponBase>();
if (!weaponBase.IsValid || weaponBase.Entity == null) continue;
var vdata = weaponBase.VData;
if (vdata == null) continue;
if (!slots.Contains(vdata.GearSlot)) continue;
weapon.Value.AddEntityIOEvent("Kill", weapon.Value);
}
}
}

View File

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

View File

@@ -1,2 +1,17 @@
ROLE_SPECTATOR: "Spectator"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{grey}!"
TASER_SCANNED: "%PREFIX%You scanned {0}{grey}, they are %an% {1}{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_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

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

View File

@@ -4,6 +4,7 @@ using TTT.API;
using TTT.API.Command;
using TTT.API.Events;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Commands;

View File

@@ -7,6 +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,9 +1,9 @@
using System.Reactive.Concurrency;
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Game;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Loggers;

View File

@@ -4,6 +4,7 @@ using TTT.API.Messages;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;

View File

@@ -2,8 +2,8 @@
namespace TTT.Game.Roles;
public class BaseWeapon(string id, int? reserve = null,
int? current = null) : IWeapon {
public class BaseWeapon(string id, int? reserve = null, int? current = null)
: IWeapon {
public string WeaponId { get; } = id;
public int? ReserveAmmo { get; } = reserve;
public int? CurrentAmmo { get; } = current;

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -1,6 +1,7 @@
using System.Drawing;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Player;
using TTT.Game.lang;
using TTT.Locale;
namespace TTT.Game.Roles;

View File

@@ -1,5 +1,6 @@
using System.Drawing;
using TTT.API.Player;
using TTT.Game.lang;
namespace TTT.Game.Roles;

View File

@@ -8,6 +8,7 @@ using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.Events.Game;
using TTT.Game.lang;
using TTT.Game.Loggers;
using TTT.Game.Roles;
using TTT.Locale;

View File

@@ -4,7 +4,7 @@ using TTT.API.Role;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.Game;
namespace TTT.Game.lang;
public static class GameMsgs {
public static IMsg PREFIX => MsgFactory.Create(nameof(PREFIX));

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Player;

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Game;
using TTT.API.Player;
@@ -35,12 +36,10 @@ public class BuyCommand(IServiceProvider provider) : ICommand {
if (info.ArgCount == 1) return CommandResult.PRINT_USAGE;
var query = string.Join(" ", info.Args.Skip(1));
info.ReplySync($"Searching for item: {query}");
var item = searchItem(query);
var item = searchItem(query);
if (item == null) {
info.ReplySync($"Item '{query}' not found.");
info.ReplySync(locale[ShopMsgs.SHOP_ITEM_NOT_FOUND(query)]);
return CommandResult.ERROR;
}

View File

@@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using CounterStrikeSharp.API.Modules.Utils;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Command;
using TTT.API.Messages;
using TTT.API.Player;
@@ -20,7 +22,8 @@ public class ListCommand(IServiceProvider provider) : ICommand {
public Task<CommandResult>
Execute(IOnlinePlayer? executor, ICommandInfo info) {
foreach (var item in shop.Items)
messenger.Message(executor, $"{item.Name} - {item.Description}");
messenger.Message(executor,
$"{ChatColors.Grey}- {ChatColors.White}{item.Name} {ChatColors.Grey}- {item.Description}");
return Task.FromResult(CommandResult.SUCCESS);
}

View File

@@ -2,7 +2,7 @@ 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;
namespace TTT.Shop.Commands;

View File

@@ -1,4 +1,5 @@
using TTT.API.Player;
using ShopAPI;
using TTT.API.Player;
using TTT.Game.Events.Player;
namespace TTT.Shop.Events;

View File

@@ -1,4 +1,5 @@
using TTT.API.Events;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Player;
namespace TTT.Shop.Events;

View File

@@ -1,22 +1,24 @@
using CounterStrikeSharp.API.Core;
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.Player;
using TTT.Game.Listeners;
using TTT.Game.Roles;
namespace TTT.Shop.Items.Detective.Stickers;
public class StickerListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly IIconManager? icons = provider.GetService<IIconManager>();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
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
@@ -33,7 +35,6 @@ public class StickerListener(IServiceProvider provider)
if (attacker == null) return;
if (!Roles.GetRoles(attacker).Any(r => r is DetectiveRole)) return;
var player = converter.GetPlayer(victim);
if (player == null || !player.IsValid) return;
icons.RevealToAll(player.Slot);

View File

@@ -1,21 +1,22 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Extensions;
using TTT.API.Game;
using TTT.API.Player;
using TTT.API.Role;
using TTT.API.Storage;
using TTT.Game.Roles;
using TTT.Locale;
namespace TTT.Shop.Items.Detective.Stickers;
public static class StickerExtensions {
public static void AddStickerServices(this IServiceCollection services) {
services.AddModBehavior<Stickers>();
services.AddModBehavior<StickerListener>();
}
}
public class Stickers(IServiceProvider provider) : BaseItem(provider) {
public class Stickers(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly StickerConfig config = provider
.GetService<IStorage<StickerConfig>>()
?.Load()
@@ -34,14 +35,7 @@ public class Stickers(IServiceProvider provider) : BaseItem(provider) {
public override void OnPurchase(IOnlinePlayer player) { }
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
if (icons == null || !Roles.GetRoles(player).Any(r => r is DetectiveRole))
return PurchaseResult.ITEM_NOT_PURCHASABLE;
if (Shop.HasItem(player, this)) return PurchaseResult.ALREADY_OWNED;
return PurchaseResult.SUCCESS;
return base.CanPurchase(player);
}
}
public record StickerConfig : ShopItemConfig {
public override int Price { get; init; } = 70;
public bool AnnounceReveals { get; init; } = false;
}

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

@@ -1,6 +1,7 @@
using CounterStrikeSharp.API;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
@@ -33,13 +34,12 @@ public class DeagleDamageListener(IServiceProvider provider)
.FirstOrDefault(s => s is OneShotDeagle);
if (deagleItem == null) return;
if (ev.Weapon != config.Weapon) {
if (ev.Weapon != config.Weapon)
// CS2 specifically causes the weapon to be "weapon_deagle" even if
// the player is holding a revolver, so we need to check for that as well
if (ev.Weapon is not "weapon_deagle"
|| !config.Weapon.Equals("weapon_revolver"))
return;
}
var attackerRole = Roles.GetRoles(attacker);
var victimRole = Roles.GetRoles(victim);

View File

@@ -8,7 +8,7 @@ public class DeagleMsgs {
public static IMsg SHOP_ITEM_DEAGLE_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_DESC));
public static IMsg SHOP_ITEM_DEAGLE_HIT_FF
=> MsgFactory.Create(nameof(SHOP_ITEM_DEAGLE_HIT_FF));
}

View File

@@ -1,14 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Extensions;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.Locale;
namespace TTT.Shop.Items;
public static class DeagleServiceCollection {
public static void AddDeagleServices(this IServiceCollection collection) {
collection.AddModBehavior<OneShotDeagle>();
collection.AddModBehavior<DeagleDamageListener>();
}
}
@@ -28,6 +30,11 @@ public class OneShotDeagle(IServiceProvider provider)
public override ShopItemConfig Config => deagleConfigStorage;
public string WeaponId => deagleConfigStorage.Weapon;
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;
public override void OnPurchase(IOnlinePlayer player) {
Task.Run(async () => {
await Inventory.RemoveWeaponInSlot(player,
@@ -39,17 +46,4 @@ public class OneShotDeagle(IServiceProvider provider)
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return PurchaseResult.SUCCESS;
}
public string WeaponId => deagleConfigStorage.Weapon;
public int? ReserveAmmo { get; init; } = 0;
public int? CurrentAmmo { get; init; } = 1;
}
public record OneShotDeagleConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public bool DoesFriendlyFire { get; init; } = true;
public bool KillShooterOnFF { get; init; } = false;
public string Weapon { get; init; } = "revolver";
public int WeaponSlot { get; init; } = 1;
}

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,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;

View File

@@ -1,5 +1,6 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.Game.Events.Game;

View File

@@ -1,11 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API;
using TTT.API.Events;
using TTT.API.Messages;
using TTT.API.Player;
using TTT.Locale;
using TTT.Shop.Events;
using TTT.Shop.Items;
namespace TTT.Shop;
@@ -26,10 +26,7 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
public ISet<IShopItem> Items { get; } = new HashSet<IShopItem>();
public bool RegisterItem(IShopItem item) {
item.Start();
return Items.Add(item);
}
public bool RegisterItem(IShopItem item) { return Items.Add(item); }
public PurchaseResult TryPurchase(IOnlinePlayer player, IShopItem item,
bool printReason = true) {

View File

@@ -16,6 +16,8 @@
<ItemGroup>
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
<ProjectReference Include="..\CS2\CS2.csproj"/>
<ProjectReference Include="..\ShopAPI\ShopAPI.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,8 +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;
@@ -18,7 +25,14 @@ public static class ShopServiceCollection {
collection.AddModBehavior<BuyCommand>();
collection.AddModBehavior<BalanceCommand>();
collection.AddC4Services();
collection.AddCamoServices();
collection.AddDamageStation();
collection.AddDeagleServices();
collection.AddDnaScannerServices();
collection.AddGlovesServices();
collection.AddHealthStation();
collection.AddM4A1Services();
collection.AddStickerServices();
}
}

View File

@@ -1,3 +1,4 @@
using ShopAPI;
using TTT.Locale;
namespace TTT.Shop;
@@ -7,6 +8,13 @@ public static class ShopMsgs {
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_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));
@@ -22,9 +30,6 @@ public static class ShopMsgs {
item.Config.Price, bal);
}
public static IMsg SHOP_CANNOT_PURCHASE
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));
public static IMsg SHOP_CANNOT_PURCHASE_WITH_REASON(string reason) {
return MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE_WITH_REASON), reason);
}

View File

@@ -1,13 +1,30 @@
SHOP_INACTIVE: "%PREFIX%The shop is currently closed."
SHOP_ITEM_NOT_FOUND: "%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!"
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_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}."
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

@@ -1,29 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using TTT.API.Player;
using TTT.API.Role;
using TTT.Locale;
namespace TTT.Shop.Items;
namespace ShopAPI;
public abstract class BaseItem(IServiceProvider provider) : IShopItem {
protected readonly IServiceProvider Provider = provider;
protected readonly IShop Shop = provider.GetRequiredService<IShop>();
protected readonly IRoleAssigner Roles =
provider.GetRequiredService<IRoleAssigner>();
protected readonly IInventoryManager Inventory =
provider.GetRequiredService<IInventoryManager>();
protected readonly IMsgLocalizer Locale =
provider.GetRequiredService<IMsgLocalizer>();
protected readonly IInventoryManager Inventory =
provider.GetRequiredService<IInventoryManager>();
protected readonly IServiceProvider Provider = provider;
public void Dispose() { }
protected readonly IRoleAssigner Roles =
provider.GetRequiredService<IRoleAssigner>();
protected readonly IShop Shop = provider.GetRequiredService<IShop>();
public virtual void Dispose() { }
public abstract string Name { get; }
public abstract string Description { get; }
public abstract ShopItemConfig Config { get; }
public abstract void OnPurchase(IOnlinePlayer player);
public abstract PurchaseResult CanPurchase(IOnlinePlayer player);
public void Start() { Shop.RegisterItem(this); }
public virtual void Start() { Shop.RegisterItem(this); }
}

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

@@ -0,0 +1,7 @@
namespace ShopAPI.Configs;
public record DnaScannerConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public int MaxSamples { get; init; } = 0;
public TimeSpan DecayTime { get; init; } = TimeSpan.FromSeconds(10);
}

View File

@@ -0,0 +1,16 @@
using System.Drawing;
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
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
return Color.FromArgb(r, g, b);
}
}

View File

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

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,9 @@
namespace ShopAPI.Configs;
public record OneShotDeagleConfig : ShopItemConfig {
public override int Price { get; init; } = 100;
public bool DoesFriendlyFire { get; init; } = true;
public bool KillShooterOnFF { get; init; } = false;
public string Weapon { get; init; } = "revolver";
public int WeaponSlot { get; init; } = 1;
}

View File

@@ -4,7 +4,7 @@ using TTT.API.Role;
using TTT.Game.Events.Player;
using TTT.Game.Roles;
namespace TTT.Shop;
namespace ShopAPI.Configs;
public record ShopConfig(IRoleAssigner assigner) {
private static readonly Type[] roleConcerns = [

View File

@@ -1,4 +1,4 @@
namespace TTT.Shop;
namespace ShopAPI.Configs;
public abstract record ShopItemConfig {
public abstract int Price { get; init; }

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

@@ -0,0 +1,19 @@
using System.Drawing;
namespace ShopAPI.Configs;
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
var r = 255; // stays at 255
var g = (int)(255 * (1 - health)); // goes from 255 → 0
var b = (int)(255 * (1 - health)); // goes from 255 → 0
return Color.FromArgb(r, g, b);
}
}

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

@@ -1,7 +1,7 @@
using TTT.API.Player;
using TTT.API.Storage;
namespace TTT.Shop;
namespace ShopAPI;
public interface IShop : IKeyedStorage<IPlayer, int>,
IKeyWritable<IPlayer, int> {
@@ -24,10 +24,15 @@ public interface IShop : IKeyedStorage<IPlayer, int>,
bool HasItem(IOnlinePlayer player, IShopItem item) {
return GetOwnedItems(player).Any(i => i.Id == item.Id);
}
bool HasItem<T>(IOnlinePlayer player) where T : IShopItem {
return GetOwnedItems(player).Any(i => i is T);
}
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

@@ -1,7 +1,8 @@
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Player;
namespace TTT.Shop;
namespace ShopAPI;
public interface IShopItem : ITerrorModule {
string Name { get; }

View File

@@ -1,4 +1,4 @@
namespace TTT.Shop;
namespace ShopAPI;
public enum PurchaseResult {
/// <summary>
@@ -22,6 +22,11 @@ public enum PurchaseResult {
/// </summary>
ITEM_NOT_PURCHASABLE,
/// <summary>
/// The player does not have the required role to purchase this item.
/// </summary>
WRONG_ROLE,
/// <summary>
/// An event canceled the purchase.
/// </summary>
@@ -41,18 +46,20 @@ public enum PurchaseResult {
public static class PurchaseResultExtensions {
public static string ToMessage(this PurchaseResult result) {
return result switch {
PurchaseResult.SUCCESS => "Purchase successful.",
PurchaseResult.SUCCESS => "Purchase successful",
PurchaseResult.INSUFFICIENT_FUNDS =>
"You do not have enough funds to complete this purchase.",
PurchaseResult.ITEM_NOT_FOUND => "The item was not found in the shop.",
"You do not have enough funds to complete this purchase",
PurchaseResult.ITEM_NOT_FOUND => "The item was not found in the shop",
PurchaseResult.ITEM_NOT_PURCHASABLE =>
"You cannot purchase this item at the moment.",
PurchaseResult.PURCHASE_CANCELED => "The purchase was canceled.",
"You cannot purchase this item at the moment",
PurchaseResult.PURCHASE_CANCELED => "The purchase was canceled",
PurchaseResult.UNKNOWN_ERROR =>
"An unknown error occurred during the purchase.",
"An unknown error occurred during the purchase",
PurchaseResult.WRONG_ROLE =>
"You do not have the required role to purchase this item",
PurchaseResult.ALREADY_OWNED =>
"You already own this item and cannot purchase it again.",
_ => "An unexpected error occurred."
"You already own this item and cannot purchase it again",
_ => "An unexpected error occurred"
};
}
}

View File

@@ -0,0 +1,13 @@
using TTT.API.Player;
using TTT.API.Role;
namespace ShopAPI;
public abstract class RoleRestrictedItem<T>(IServiceProvider provider)
: BaseItem(provider) where T : IRole {
public override PurchaseResult CanPurchase(IOnlinePlayer player) {
return Roles.GetRoles(player).Any(r => r is T) ?
PurchaseResult.SUCCESS :
PurchaseResult.WRONG_ROLE;
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\API\API.csproj"/>
<ProjectReference Include="..\Game\Game.csproj"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using System.Drawing;
using ShopAPI.Configs;
namespace ShopAPI;
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 float MaxRange { get; init; } = 256;
public virtual TimeSpan HealthInterval { get; init; } =
TimeSpan.FromSeconds(1);
public abstract string UseSound { get; init; }
public abstract Color GetColor(float health);
}

View File

@@ -2,8 +2,8 @@ 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;
using Xunit;

View File

@@ -9,8 +9,7 @@ public class TTTTest(IServiceProvider provider)
public void Command_ShouldPrint_Version() {
var player = TestPlayer.Random();
Commands.ProcessCommand(new TestCommandInfo(Provider, player,
Command.Id));
Commands.ProcessCommand(new TestCommandInfo(Provider, player, Command.Id));
Assert.Single(player.Messages);
Assert.Contains(Command.Version, player.Messages.First());

View File

@@ -3,7 +3,7 @@ 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,8 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Events;
using TTT.API.Game;
using TTT.API.Player;
using TTT.Shop;
using TTT.Shop.Listeners;
using Xunit;

Some files were not shown because too many files have changed in this diff Show More