Compare commits

...

41 Commits

Author SHA1 Message Date
MSWS
63cd3e782f Apply default config to body paint 2025-10-02 23:44:00 -07:00
MSWS
a1acb91151 Register body paint service 2025-10-02 23:42:59 -07:00
MSWS
355f39a12a refactor: Add Body Paint, refactor namespaces +semver:minor (resolves #82)
```
- Add descriptions and labels for "Body Paint" in English language file
- Update Detective's DNA Scanner and Sticker configurations for improved namespace specificity
- Implement BodyPaintMsgs class for handling body paint item messages
- Introduce BodyPaintListener for handling body paint purchase and usage events
- Restructure PlayerEvent files to align with new ShopAPI directory and namespace
- Add BodyPaintConfig for shop item configuration with default settings
```
2025-10-02 23:29:11 -07:00
MSWS
87815d099e Append Item to 1ShotDeagle Class, start work on BodyPaintItem 2025-10-02 23:08:00 -07:00
MSWS
6f904df564 Fix role assignment logging 2025-10-02 21:18:39 -07:00
MSWS
42a2fc2caf Fix gloves item (ref #81) 2025-10-02 18:49:43 -07:00
MSWS
306be07e8f Reformat and Cleanup 2025-10-02 18:30:21 -07:00
MSWS
233396fbd1 refactor: Refactor round timer and remove C4 listener logic
- Adjust handling of round win conditions in `RoundTimerListener.cs` by adding commands to ignore and re-enable conditions during round end process; improve round end subscription logic and add `Dispose` method to clean up resources.
- Remove addition of `C4Listener` from the service collection in `C4ShopItem.cs`.
- Delete `C4Listener.cs`, removing event handlers for bomb-related events.
2025-10-02 15:50:08 -07:00
MSWS
a1194008ad feat: Refactor name formatting and add C4 event handling +semver:minor
```
- Refactor `CS2Player.cs` to improve name formatting with `CreatePaddedName`, eliminating redundant methods and enhancing alignment.
- Integrate `C4Listener` into `C4ShopItem.cs` for improved C4 item handling in the shop.
- Comment out the `RoundEnd_GameEndHandler` registration in `CS2ServiceCollection.cs` to potentially defer its functional roles.
- Refactor `EndRound` method in `RoundUtil.cs` to utilize `VirtualFunctions.TerminateRoundFunc`, comment out obsolete implementations.
- Update `RoundTimerListener.cs` to incorporate `System.Reactive.Concurrency`, optimize round timing logic, and introduce resource disposal to prevent leaks.
- Implement `C4Listener.cs` to handle bomb events and announce activities, enhancing player communication.
- Simplify `StationItem.cs` by removing unused imports and redundant debug announcements while maintaining core functionality.
- Adjust `RoundBasedGame.cs` to ensure the game state is accurately set to `IN_PROGRESS` post player preparation, without changing game flow logic.
```
2025-10-02 15:21:43 -07:00
MSWS
5bc52acf3c Fix shop events not being called 2025-10-02 13:35:42 -07:00
MSWS
9a6af7acab feat: Add M4A1Config support to storage and plugin modules +semver:minor
- Add support for M4A1Config storage in CS2ServiceCollection by including a new `AddModBehavior` for `IStorage<M4A1Config>`
- Implement a new configuration class for the M4A1 shop item in CS2M4A1Config with storage and plugin module interfaces
- Define static configuration variables with validators for price, clear slots, and associated weapons in CS2M4A1Config
- Implement plugin initialization methods and register fake config variables in CS2M4A1Config
- Load configuration in CS2M4A1Config, parsing clear slots and weapons from strings into arrays, with nullable type inference and asynchronous processing
2025-10-02 13:08:31 -07:00
MSWS
8cc241bcca Fix camo applying asynchronously 2025-10-02 13:05:12 -07:00
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
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
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
121 changed files with 2150 additions and 337 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,8 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<IStorage<ShopConfig>, CS2ShopConfig>();
collection
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
collection.AddModBehavior<IStorage<M4A1Config>, CS2M4A1Config>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();
@@ -50,7 +52,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<DamageCanceler>();
collection.AddModBehavior<PlayerConnectionsHandler>();
collection.AddModBehavior<PropMover>();
collection.AddModBehavior<RoundEnd_GameEndHandler>();
// collection.AddModBehavior<RoundEnd_GameEndHandler>();
collection.AddModBehavior<RoundStart_GameStartHandler>();
// Damage Cancelers

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

@@ -0,0 +1,54 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Cvars.Validators;
using ShopAPI.Configs;
using TTT.API;
using TTT.API.Storage;
using TTT.CS2.Validators;
namespace TTT.CS2.Configs.ShopItems;
public class CS2M4A1Config : IStorage<M4A1Config>, IPluginModule {
public static readonly FakeConVar<int> CV_PRICE = new(
"css_ttt_shop_m4a1_price", "Price of the M4A1 item", 90,
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(0, 10000));
public static readonly FakeConVar<string> CV_CLEAR_SLOTS = new(
"css_ttt_shop_m4a1_clear_slots",
"Slots to clear when granting M4A1 (comma-separated ints)", "0,1");
public static readonly FakeConVar<string> CV_WEAPONS = new(
"css_ttt_shop_m4a1_weapons",
"Weapons granted with this item (comma-separated names)",
"weapon_m4a1,weapon_usp_silencer", ConVarFlags.FCVAR_NONE,
new ItemValidator(allowMultiple: true));
public void Dispose() { }
public void Start() { }
public void Start(BasePlugin? plugin) {
ArgumentNullException.ThrowIfNull(plugin, nameof(plugin));
plugin.RegisterFakeConVars(this);
}
public Task<M4A1Config?> Load() {
var slots = CV_CLEAR_SLOTS.Value.Split(',')
.Select(s => s.Trim())
.Where(s => int.TryParse(s, out _))
.Select(int.Parse)
.ToArray();
var weapons = CV_WEAPONS.Value.Split(',')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
var cfg = new M4A1Config {
Price = CV_PRICE.Value, ClearSlots = slots, Weapons = weapons
};
return Task.FromResult<M4A1Config?>(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,35 @@
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;
using TTT.Shop.Items.Traitor.BodyPaint;
namespace TTT.CS2.Items.BodyPaint;
public static class BodyPaintServicesCollection {
public static void AddBodyPaintServices(this IServiceCollection collection) {
collection.AddModBehavior<BodyPaintItem>();
collection.AddModBehavior<BodyPaintListener>();
}
}
public class BodyPaintItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider) {
private readonly BodyPaintConfig config = provider
.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
public override string Name => Locale[BodyPaintMsgs.SHOP_ITEM_BODY_PAINT];
public override string Description
=> Locale[BodyPaintMsgs.SHOP_ITEM_BODY_PAINT_DESC];
public override ShopItemConfig Config => config;
public override void OnPurchase(IOnlinePlayer player) { }
}

View File

@@ -0,0 +1,59 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Events;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;
using TTT.CS2.API;
using TTT.CS2.Extensions;
using TTT.Game.Events.Body;
using TTT.Game.Listeners;
using TTT.Shop.Items.Traitor.BodyPaint;
namespace TTT.CS2.Items.BodyPaint;
public class BodyPaintListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly BodyPaintConfig config =
provider.GetService<IStorage<BodyPaintConfig>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new BodyPaintConfig();
private readonly Dictionary<IPlayer, int> uses = new();
private readonly IShop shop = provider.GetRequiredService<IShop>();
private readonly IBodyTracker bodies =
provider.GetRequiredService<IBodyTracker>();
[UsedImplicitly]
[EventHandler]
public void OnPurchase(PlayerPurchaseItemEvent ev) {
if (ev.Item is not BodyPaintItem) return;
if (ev.Player is not IOnlinePlayer online) return;
uses[online] += config.MaxUses;
}
[UsedImplicitly]
[EventHandler]
public void BodyIdentify(BodyIdentifyEvent ev) {
if (!bodies.Bodies.TryGetValue(ev.Body, out var body)) return;
if (ev.Identifier == null || !usePaint(ev.Identifier)) return;
ev.IsCanceled = true;
body.SetColor(config.ColorToApply);
}
private bool usePaint(IPlayer player) {
if (player is not IOnlinePlayer online) return false;
if (!uses.TryGetValue(player, out var useCount)) return false;
if (useCount <= 0) return false;
uses[player] = useCount - 1;
if (uses[player] > 0) return true;
shop.RemoveItem<BodyPaintItem>(online);
Messenger.Message(online, Locale[BodyPaintMsgs.SHOP_ITEM_BODY_PAINT_OUT]);
return true;
}
}

View File

@@ -0,0 +1,14 @@
using TTT.Locale;
namespace TTT.Shop.Items.Traitor.BodyPaint;
public class BodyPaintMsgs {
public static IMsg SHOP_ITEM_BODY_PAINT
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT));
public static IMsg SHOP_ITEM_BODY_PAINT_DESC
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_DESC));
public static IMsg SHOP_ITEM_BODY_PAINT_OUT
=> MsgFactory.Create(nameof(SHOP_ITEM_BODY_PAINT_OUT));
}

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,45 @@
using System.Drawing;
using CounterStrikeSharp.API;
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) {
Server.NextWorldUpdate(() => {
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,95 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
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, explanation);
}
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,37 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
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,75 @@
using CounterStrikeSharp.API.Core;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Traitor;
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,58 @@
using CounterStrikeSharp.API;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
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,141 @@
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.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>>();
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();
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

@@ -1,14 +1,15 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using TTT.API.Events;
using TTT.API.Player;
using TTT.CS2.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;
@@ -25,6 +26,7 @@ public class BodyPickupListener(IServiceProvider provider)
private readonly IAliveSpoofer? spoofer =
provider.GetService<IAliveSpoofer>();
[UsedImplicitly]
[EventHandler]
public void OnPropPickup(PropPickupEvent ev) {
if (!bodies.TryLookup(ev.Prop.Index.ToString(), out var body)) return;
@@ -35,10 +37,10 @@ public class BodyPickupListener(IServiceProvider provider)
var identifyEvent = new BodyIdentifyEvent(body, online);
Bus.Dispatch(identifyEvent);
if (identifyEvent.IsCanceled) return;
}
[EventHandler]
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnIdentify(BodyIdentifyEvent ev) {
ev.Body.IsIdentified = true;

View File

@@ -1,4 +1,5 @@
using System.Drawing;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
@@ -30,6 +31,11 @@ public class RoundTimerListener(IServiceProvider provider)
private readonly IPlayerConverter<CCSPlayerController> converter =
provider.GetRequiredService<IPlayerConverter<CCSPlayerController>>();
private readonly IScheduler scheduler = provider
.GetRequiredService<IScheduler>();
private IDisposable? endTimer;
[UsedImplicitly]
[EventHandler(IgnoreCanceled = true)]
public void OnRoundStart(GameStateUpdateEvent ev) {
@@ -49,12 +55,21 @@ public class RoundTimerListener(IServiceProvider provider)
return;
}
if (ev.NewState == State.FINISHED) endTimer?.Dispose();
if (ev.NewState != State.IN_PROGRESS) return;
var duration = config.RoundCfg.RoundDuration(ev.Game.Players.Count);
Messenger.DebugAnnounce("Total duration: {0} for {1} player", duration,
ev.Game.Players.Count);
Server.NextWorldUpdate(() => {
RoundUtil.SetTimeRemaining((int)config.RoundCfg
.RoundDuration(ev.Game.Players.Count)
.TotalSeconds);
Server.ExecuteCommand("mp_ignore_round_win_conditions 0");
RoundUtil.SetTimeRemaining((int)duration.TotalSeconds);
});
endTimer?.Dispose();
endTimer = scheduler.Schedule(duration, () => {
Server.NextWorldUpdate(() => {
Messenger.DebugAnnounce("Time is up!");
ev.Game.EndGame(EndReason.TIMEOUT(new InnocentRole(provider)));
});
});
}
@@ -65,11 +80,6 @@ public class RoundTimerListener(IServiceProvider provider)
revealRoles(ev.Game);
// If CS caused the round to end, we will have 0 time left
// in this case, CS automatically handles the end of round stuff
// so we don't need to do anything
if (RoundUtil.GetTimeRemaining() <= 1) return;
Server.NextWorldUpdate(() => {
var endReason = endRound(ev);
@@ -81,8 +91,11 @@ public class RoundTimerListener(IServiceProvider provider)
var timer = Observable.Timer(
config.RoundCfg.TimeBetweenRounds, Scheduler);
timer.Subscribe(_
=> Server.NextWorldUpdate(() => RoundUtil.EndRound(endReason)));
timer.Subscribe(_ => Server.NextWorldUpdate(() => {
Server.ExecuteCommand("mp_ignore_round_win_conditions 1");
RoundUtil.EndRound(endReason);
Server.ExecuteCommand("mp_ignore_round_win_conditions 0");
}));
});
}
@@ -115,4 +128,10 @@ public class RoundTimerListener(IServiceProvider provider)
new EventNextlevelChanged(true).FireEvent(false);
}
public override void Dispose() {
base.Dispose();
endTimer?.Dispose();
}
}

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

@@ -112,15 +112,29 @@ public class CS2Player : IOnlinePlayer {
// Goal: Pad the name to a fixed width for better alignment in logs
// Left-align ID, right-align name
private string createPaddedName() {
var idPart = $"({getSuffix(Id, 5)})";
var effectivePadding = namePadding - idPart.Length;
var namePart = Name.Length >= effectivePadding ?
getSuffix(Name, effectivePadding) :
Name.PadLeft(effectivePadding);
return $"{idPart} {namePart}";
return CreatePaddedName(Id, Name, namePadding + 8);
}
private string getSuffix(string s, int len) {
return s.Length <= len ? s : s[^len..];
public static string CreatePaddedName(string id, string name, int len) {
var suffix = id.Length > 5 ? id[^5..] : id.PadLeft(5, '0');
var prefix = $"({suffix})";
var baseStr = $"{prefix} {name}";
if (baseStr.Length == len) return baseStr;
if (baseStr.Length < len) {
// Pad spaces so the name ends up right-aligned
var padding = len - (prefix.Length + name.Length);
return prefix + new string(' ', padding + 1) + name;
}
// Too long, cut off from the end of the name
var availableForName = len - (prefix.Length + 1);
if (availableForName < 0) availableForName = 0;
var trimmedName = name.Length > availableForName ?
name[..availableForName] :
name;
return $"{prefix} {trimmedName}";
}
}

View File

@@ -1,16 +1,16 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Entities.Constants;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Memory;
using CounterStrikeSharp.API.Modules.Utils;
namespace TTT.CS2.Utils;
public static class RoundUtil {
private static readonly
MemoryFunctionVoid<nint, float, RoundEndReason, nint, nint>
TerminateRoundFunc =
new(GameData.GetSignature("CCSGameRules_TerminateRound"));
// private static readonly
// MemoryFunctionVoid<nint, float, RoundEndReason, nint, nint>
// TerminateRoundFunc =
// new(GameData.GetSignature("CCSGameRules_TerminateRound"));
private static IEnumerable<CCSTeam>? _teamManager;
@@ -52,7 +52,9 @@ public static class RoundUtil {
var gameRules = ServerUtil.GameRulesProxy;
if (gameRules == null || gameRules.GameRules == null) return;
// TODO: Figure out what these params do
TerminateRoundFunc.Invoke(gameRules.GameRules.Handle, 5f, reason, 0, 0);
// TerminateRoundFunc.Invoke(gameRules.GameRules.Handle, 5f, reason, 0, 0);
VirtualFunctions.TerminateRoundFunc.Invoke(gameRules.GameRules.Handle,
reason, 5f, 0, 0);
}
public static void SetTeamScore(CsTeam team, int score) {

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,21 @@
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."
SHOP_ITEM_BODY_PAINT: "Body Paint"
SHOP_ITEM_BODY_PAINT_DESC: "Paint bodies to make them appear identified."
SHOP_ITEM_BODY_PAINT_OUT: "%PREFIX% You ran out of body paint."

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

@@ -9,23 +9,25 @@ namespace TTT.Game.Listeners.Loggers;
public class PlayerActionsLogger(IServiceProvider provider)
: BaseListener(provider) {
// Needs to be higher so we detect the kill before the game ends
[EventHandler(Priority = Priority.HIGH)]
[UsedImplicitly]
[EventHandler(Priority = Priority.HIGH)]
public void OnPlayerKill(PlayerDeathEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
Games.ActiveGame.Logger.LogAction(new DeathAction(Provider, ev));
}
[UsedImplicitly]
[EventHandler]
public void OnPlayerDamage(PlayerDamagedEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
Games.ActiveGame.Logger.LogAction(new DamagedAction(Provider, ev));
}
[UsedImplicitly]
[EventHandler]
public void OnPlayerAssignedRole(PlayerRoleAssignEvent ev) {
if (Games.ActiveGame is not { State: State.IN_PROGRESS }) return;
Games.ActiveGame.Logger.LogAction(
Games.ActiveGame?.Logger.LogAction(
new RoleAssignedAction(ev.Player, ev.Role));
}
}

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;
@@ -171,7 +172,6 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
return;
}
State = State.IN_PROGRESS;
foreach (var player in online) inventory.RemoveAllWeapons(player);
@@ -179,6 +179,8 @@ public class RoundBasedGame(IServiceProvider provider) : IGame {
RoleAssigner.AssignRoles(online, Roles);
players.AddRange(online);
State = State.IN_PROGRESS;
var traitors = ((IGame)this).GetAlive(typeof(TraitorRole)).Count;
var nonTraitors = online.Count - traitors;
Messenger?.MessageAll(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;
@@ -20,34 +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));
info.ReplySync($"Searching for item: {query}");
var item = searchItem(query);
var item = searchItem(query);
if (item == null) {
info.ReplySync($"Item '{query}' not found.");
return CommandResult.ERROR;
info.ReplySync(locale[ShopMsgs.SHOP_ITEM_NOT_FOUND(query)]);
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

@@ -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,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

View File

@@ -1,11 +1,11 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using ShopAPI.Configs.Detective;
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;
@@ -16,7 +16,8 @@ public static class StickerExtensions {
}
}
public class Stickers(IServiceProvider provider) : BaseItem(provider) {
public class Stickers(IServiceProvider provider)
: RoleRestrictedItem<DetectiveRole>(provider) {
private readonly StickerConfig config = provider
.GetService<IStorage<StickerConfig>>()
?.Load()
@@ -35,13 +36,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.WRONG_ROLE;
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;
}

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;
@@ -30,16 +31,15 @@ public class DeagleDamageListener(IServiceProvider provider)
if (attacker == null) return;
var deagleItem = shop.GetOwnedItems(attacker)
.FirstOrDefault(s => s is OneShotDeagle);
.FirstOrDefault(s => s is OneShotDeagleItem);
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,20 +1,21 @@
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<OneShotDeagleItem>();
collection.AddModBehavior<DeagleDamageListener>();
}
}
public class OneShotDeagle(IServiceProvider provider)
public class OneShotDeagleItem(IServiceProvider provider)
: BaseItem(provider), IWeapon {
private readonly OneShotDeagleConfig deagleConfigStorage = provider
.GetService<IStorage<OneShotDeagleConfig>>()
@@ -29,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,
@@ -40,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,72 @@
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;
namespace TTT.Shop.Items.Traitor.Gloves;
public class GlovesListener(IServiceProvider provider)
: BaseListener(provider) {
private readonly GlovesConfig config =
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 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], config.MaxUses)]);
}
[UsedImplicitly]
[EventHandler(Priority =
Priority.HIGH)] // High priority to cancel before other handlers
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],
config.MaxUses)]);
}
[UsedImplicitly]
[EventHandler]
public void OnGameState(GameStateUpdateEvent ev) {
if (ev.NewState != State.IN_PROGRESS) return;
uses.Clear();
}
private bool useGloves(IPlayer player) {
if (player is not IOnlinePlayer online) return false;
if (!uses.TryGetValue(player, out var useCount)) {
if (!shop.HasItem<GlovesItem>(online)) return false;
uses[player] = config.MaxUses - 1;
return true;
}
if (useCount <= 0) return false;
uses[player] = useCount - 1;
if (useCount - 1 > 0) 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,7 @@
using Microsoft.Extensions.DependencyInjection;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
using TTT.API.Events;
using TTT.API.Player;
using TTT.API.Storage;
@@ -15,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

@@ -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 ShopAPI.Events;
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,9 +26,7 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
public ISet<IShopItem> Items { get; } = new HashSet<IShopItem>();
public bool RegisterItem(IShopItem item) {
return Items.Add(item);
}
public bool RegisterItem(IShopItem item) { return Items.Add(item); }
public PurchaseResult TryPurchase(IOnlinePlayer player, IShopItem item,
bool printReason = true) {
@@ -56,8 +54,15 @@ public class Shop(IServiceProvider provider) : ITerrorModule, IShop {
return canPurchase;
}
balances[player.Id] = bal - cost;
var purchaseEvent = new PlayerPurchaseItemEvent(player, item);
bus.Dispatch(purchaseEvent);
if (purchaseEvent.IsCanceled) return PurchaseResult.PURCHASE_CANCELED;
AddBalance(player, -cost, item.Name);
GiveItem(player, item);
if (printReason)
messenger?.Message(player, localizer[ShopMsgs.SHOP_PURCHASED(item)]);
return PurchaseResult.SUCCESS;
}

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

@@ -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,8 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using TTT.API.Extensions;
using TTT.CS2.Items.BodyPaint;
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 +26,15 @@ public static class ShopServiceCollection {
collection.AddModBehavior<BuyCommand>();
collection.AddModBehavior<BalanceCommand>();
collection.AddBodyPaintServices();
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,17 @@ 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_PURCHASED(IShopItem item) {
return 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));
@@ -22,9 +34,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,31 @@
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}."
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

@@ -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,9 @@
using System.Drawing;
namespace ShopAPI.Configs;
public record BodyPaintConfig : ShopItemConfig {
public override int Price { get; init; } = 60;
public int MaxUses { get; init; } = 1;
public Color ColorToApply { get; init; } = Color.GreenYellow;
}

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.Detective;
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.Detective;
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.Detective;
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.Traitor;
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

@@ -2,7 +2,7 @@
using TTT.API.Player;
using TTT.Game.Events.Player;
namespace TTT.Shop.Events;
namespace ShopAPI.Events;
public class PlayerBalanceEvent(IPlayer player, int oldBalance, int newBalance,
string? reason) : PlayerEvent(player), ICancelableEvent {

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