Compare commits

...

8 Commits

Author SHA1 Message Date
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
MSWS
67755c36c6 Tweak AI prompt and input 2025-09-30 18:25:18 -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
18 changed files with 207 additions and 20 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

@@ -39,6 +39,7 @@ public static class CS2ServiceCollection {
collection.AddModBehavior<IStorage<ShopConfig>, CS2ShopConfig>();
collection
.AddModBehavior<IStorage<OneShotDeagleConfig>, CS2OneShotDeagleConfig>();
collection.AddModBehavior<IStorage<C4Config>, CS2C4Config>();
// TTT - CS2 Specific optionals
collection.AddScoped<ITextSpawner, TextSpawner>();

View File

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

View File

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

View File

@@ -22,10 +22,10 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
?.Load()
.GetAwaiter()
.GetResult() ?? new DamageStationConfig()) {
public override string Name => "Damage Station";
public override string Name => Locale[StationMsgs.SHOP_ITEM_STATION_HURT];
public override string Description
=> "Deployable damage station that harms nearby traitors over time.";
=> Locale[StationMsgs.SHOP_ITEM_STATION_HURT_DESC];
private readonly IRoleAssigner roles =
provider.GetRequiredService<IRoleAssigner>();
@@ -68,7 +68,7 @@ public class DamageStation(IServiceProvider provider) : StationItem(provider,
player.Health += damageAmount;
info.HealthGiven += damageAmount;
gamePlayer.ExecuteClientCommand("play sounds/buttons/blip2");
gamePlayer.ExecuteClientCommand("play " + _Config.UseSound);
}
}
}

View File

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

View File

@@ -60,7 +60,7 @@ public abstract class StationItem(IServiceProvider provider,
new CounterStrikeSharp.API.Modules.Utils.Vector(ev.X, ev.Y, ev.Z);
var nearest = props
.Select(kv => (Key: kv.Key, Value: kv.Value,
.Select(kv => (kv.Key, kv.Value,
Distance: kv.Key.AbsOrigin!.DistanceSquared(hitVec)))
.Where(t => t.Key is { IsValid: true, AbsOrigin: not null })
.OrderBy(t => t.Distance)
@@ -129,9 +129,10 @@ public abstract class StationItem(IServiceProvider provider,
|| gamePlayer.Pawn.Value == null)
return;
var spawnPos = gamePlayer.Pawn.Value.AbsOrigin.Clone();
if (spawnPos != null) {
if (gamePlayer.PlayerPawn.Value != null)
spawnPos += gamePlayer.PlayerPawn.Value?.EyeAngles.ToForward()! * 8;
if (spawnPos != null && gamePlayer.PlayerPawn.Value != null) {
var forward = gamePlayer.PlayerPawn.Value.EyeAngles.ToForward();
forward.Z = 0;
spawnPos += forward.Normalized() * 8;
}
prop.Teleport(spawnPos);

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

View File

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

View File

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

View File

@@ -0,0 +1,66 @@
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
using ShopAPI;
using ShopAPI.Configs;
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;
using TTT.Shop.Items.Traitor.C4;
namespace TTT.Shop.Items.Traitor;
public static class C4ServiceCollection {
public static void AddC4Services(this IServiceCollection collection) {
collection.AddModBehavior<C4ShopItem>();
}
}
public class C4ShopItem(IServiceProvider provider)
: RoleRestrictedItem<TraitorRole>(provider), IListener {
public override string Name => Locale[C4Msgs.SHOP_ITEM_C4];
public override string Description => Locale[C4Msgs.SHOP_ITEM_C4_DESC];
private int c4sBought;
private readonly C4Config config = provider.GetService<IStorage<C4Config>>()
?.Load()
.GetAwaiter()
.GetResult() ?? new C4Config();
private readonly IPlayerFinder finder =
provider.GetRequiredService<IPlayerFinder>();
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

@@ -6,6 +6,7 @@ using TTT.CS2.Items.Station;
using TTT.Shop.Commands;
using TTT.Shop.Items;
using TTT.Shop.Items.Detective.Stickers;
using TTT.Shop.Items.Traitor;
using TTT.Shop.Listeners;
namespace TTT.Shop;
@@ -26,5 +27,6 @@ public static class ShopServiceCollection {
collection.AddDnaScannerServices();
collection.AddHealthStation();
collection.AddDamageStation();
collection.AddC4Services();
}
}

View File

@@ -9,9 +9,13 @@ 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_INSUFFICIENT_BALANCE: "%PREFIX%You cannot afford {white}{0}{grey}, it costs {yellow}{1}{grey} credit%s%, and you have {yellow}{2}{grey}."
SHOP_CANNOT_PURCHASE: "%PREFIX%You cannot purchase this item."
SHOP_CANNOT_PURCHASE_WITH_REASON: "%PREFIX%You cannot purchase this item: {red}{0}{grey}."
CREDITS_NAME: "credit"
CREDITS_GIVEN: "%PREFIX%{0}{1} %CREDITS_NAME%%s%"
CREDITS_GIVEN_REASON: "%PREFIX%{0}{1} %CREDITS_NAME%%s% {grey}({white}{2}{grey})"

View File

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

View File

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

View File

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

View File

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