mirror of
https://github.com/MSWS/TTT.git
synced 2025-12-06 06:22:44 -08:00
feat: Add rich and low grav rounds
This commit is contained in:
@@ -21,4 +21,55 @@ public interface IPlayerFinder {
|
||||
var matches = GetOnline().Where(p => p.Name.Contains(name)).ToList();
|
||||
return matches.Count == 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
List<IOnlinePlayer> GetMulti(string query, out string name,
|
||||
IOnlinePlayer? executor = null) {
|
||||
var result = query switch {
|
||||
"@all" => GetOnline().ToList(),
|
||||
"@me" => executor != null ? new List<IOnlinePlayer> { executor } : [],
|
||||
"@!me" => executor != null ?
|
||||
GetOnline().Where(p => p.Id != executor.Id).ToList() :
|
||||
GetOnline().ToList(),
|
||||
_ => GetSingle(query) != null ?
|
||||
new List<IOnlinePlayer> { GetSingle(query)! } : []
|
||||
};
|
||||
|
||||
name = "no players found";
|
||||
name = query switch {
|
||||
"@all" => "all players",
|
||||
"@me" => executor != null ? executor.Name : "no one",
|
||||
"@!me" => executor != null ?
|
||||
$"all players except {executor.Name}" :
|
||||
"all players",
|
||||
_ => GetSingle(query) != null ?
|
||||
GetSingle(query)!.Name :
|
||||
"no players found"
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
IOnlinePlayer? GetSingle(string query) {
|
||||
if (query.StartsWith("#")) {
|
||||
var id = query[1..];
|
||||
var byId = GetPlayerById(id);
|
||||
if (byId != null) return byId;
|
||||
var byName = GetOnline().FirstOrDefault(p => p.Name == id);
|
||||
return byName;
|
||||
}
|
||||
|
||||
var byNameExact = GetOnline().FirstOrDefault(p => p.Name == query);
|
||||
if (byNameExact != null) return byNameExact;
|
||||
|
||||
var contains = GetOnline().Where(p => p.Name.Contains(query)).ToList();
|
||||
|
||||
if (contains.Count == 1) return contains[0];
|
||||
|
||||
contains = GetOnline()
|
||||
.Where(p
|
||||
=> p.Name.Contains(query, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
return contains.Count == 1 ? contains[0] : null;
|
||||
}
|
||||
}
|
||||
@@ -33,21 +33,14 @@ public class GiveItemCommand(IServiceProvider provider) : ICommand {
|
||||
return Task.FromResult(CommandResult.ERROR);
|
||||
}
|
||||
|
||||
var target = executor;
|
||||
List<IOnlinePlayer> targets = [executor];
|
||||
|
||||
Server.NextWorldUpdateAsync(() => {
|
||||
if (info.ArgCount == 3) {
|
||||
var result = finder.GetPlayerByName(info.Args[2]);
|
||||
if (result == null) {
|
||||
info.ReplySync($"Player '{info.Args[2]}' not found.");
|
||||
return;
|
||||
}
|
||||
var name = executor.Name;
|
||||
if (info.ArgCount == 3) targets = finder.GetMulti(info.Args[2], out name);
|
||||
foreach (var player in targets) shop.GiveItem(player, item);
|
||||
|
||||
target = result;
|
||||
}
|
||||
|
||||
shop.GiveItem(target, item);
|
||||
info.ReplySync($"Gave item '{item.Name}' to {target.Name}.");
|
||||
info.ReplySync($"Gave item '{item.Name}' to {name}.");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
|
||||
private readonly IEventBus bus = provider.GetRequiredService<IEventBus>();
|
||||
|
||||
private readonly IPlayerFinder finder =
|
||||
provider.GetRequiredService<IPlayerFinder>();
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public string Id => "setrole";
|
||||
@@ -24,7 +27,10 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
Execute(IOnlinePlayer? executor, ICommandInfo info) {
|
||||
if (executor == null) return Task.FromResult(CommandResult.PLAYER_ONLY);
|
||||
|
||||
IRole roleToAssign = new TraitorRole(provider);
|
||||
// IOnlinePlayer targetPlayer = executor;
|
||||
List<IOnlinePlayer> targets = [executor];
|
||||
var targetName = executor.Name;
|
||||
IRole roleToAssign = new TraitorRole(provider);
|
||||
if (info.ArgCount == 2)
|
||||
switch (info.Args[1].ToLowerInvariant()) {
|
||||
case "d" or "det" or "detective" or "ct":
|
||||
@@ -33,18 +39,30 @@ public class SetRoleCommand(IServiceProvider provider) : ICommand {
|
||||
case "i" or "inn" or "innocent":
|
||||
roleToAssign = new InnocentRole(provider);
|
||||
break;
|
||||
default:
|
||||
targets = finder.GetMulti(info.Args[1], out targetName, executor);
|
||||
break;
|
||||
}
|
||||
|
||||
if (info.ArgCount == 3) {
|
||||
targets = finder.GetMulti(info.Args[2], out targetName, executor);
|
||||
}
|
||||
|
||||
Server.NextWorldUpdate(() => {
|
||||
var ev = new PlayerRoleAssignEvent(executor, roleToAssign);
|
||||
bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) {
|
||||
info.ReplySync("Role assignment was canceled.");
|
||||
return;
|
||||
foreach (var player in targets) {
|
||||
var ev = new PlayerRoleAssignEvent(player, roleToAssign);
|
||||
bus.Dispatch(ev);
|
||||
if (ev.IsCanceled) {
|
||||
info.ReplySync("Role assignment was canceled.");
|
||||
return;
|
||||
}
|
||||
|
||||
assigner.Write(player, [ev.Role]);
|
||||
ev.Role.OnAssign(player);
|
||||
}
|
||||
|
||||
assigner.Write(executor, [ev.Role]);
|
||||
ev.Role.OnAssign(executor);
|
||||
info.ReplySync(
|
||||
"Assigned " + roleToAssign.Name + " to " + targetName + ".");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SpecialRoundCommand(IServiceProvider provider) : ICommand {
|
||||
}
|
||||
|
||||
Server.NextWorldUpdate(() => {
|
||||
tracker.TryStartSpecialRound(round);
|
||||
tracker.TryStartSpecialRound([round]);
|
||||
info.ReplySync($"Started special round '{roundName}'.");
|
||||
});
|
||||
return Task.FromResult(CommandResult.SUCCESS);
|
||||
|
||||
@@ -44,7 +44,7 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
// Karma deltas
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_TRAITOR = new(
|
||||
"css_ttt_karma_inno_on_traitor",
|
||||
"Karma gained when Innocent kills a Traitor", 4, ConVarFlags.FCVAR_NONE,
|
||||
"Karma gained when Innocent kills a Traitor", 2, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_DETECTIVE = new(
|
||||
@@ -55,21 +55,21 @@ public class CS2KarmaConfig : IStorage<KarmaConfig>, IPluginModule {
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO_VICTIM = new(
|
||||
"css_ttt_karma_inno_on_inno_victim",
|
||||
"Karma gained or lost when Innocent kills another Innocent who was a victim",
|
||||
-1, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
-2, ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_INNO = new(
|
||||
"css_ttt_karma_inno_on_inno",
|
||||
"Karma lost when Innocent kills another Innocent", -5,
|
||||
"Karma lost when Innocent kills another Innocent", -8,
|
||||
ConVarFlags.FCVAR_NONE, new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_TRAITOR_ON_TRAITOR = new(
|
||||
"css_ttt_karma_traitor_on_traitor",
|
||||
"Karma lost when Traitor kills another Traitor", -6, ConVarFlags.FCVAR_NONE,
|
||||
"Karma lost when Traitor kills another Traitor", -12, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_INNO_ON_DETECTIVE = new(
|
||||
"css_ttt_karma_inno_on_detective",
|
||||
"Karma lost when Innocent kills a Detective", -8, ConVarFlags.FCVAR_NONE,
|
||||
"Karma lost when Innocent kills a Detective", -15, ConVarFlags.FCVAR_NONE,
|
||||
new RangeValidator<int>(-50, 50));
|
||||
|
||||
public static readonly FakeConVar<int> CV_KARMA_PER_ROUND = new(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Commands.Targeting;
|
||||
using TTT.API.Player;
|
||||
|
||||
namespace TTT.CS2.Player;
|
||||
|
||||
@@ -38,6 +38,7 @@ public class C4ShopItem(IServiceProvider provider)
|
||||
public override ShopItemConfig Config => config;
|
||||
|
||||
public override void OnPurchase(IOnlinePlayer player) {
|
||||
c4sBought++;
|
||||
Inventory.GiveWeapon(player, new BaseWeapon(config.Weapon));
|
||||
}
|
||||
|
||||
@@ -47,10 +48,8 @@ public class C4ShopItem(IServiceProvider provider)
|
||||
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))
|
||||
if (finder.GetOnline().Count(p => Shop.HasItem<C4ShopItem>(p))
|
||||
> config.MaxC4AtOnce)
|
||||
return PurchaseResult.ITEM_NOT_PURCHASABLE;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using TTT.API.Game;
|
||||
using TTT.API.Player;
|
||||
using TTT.API.Storage;
|
||||
using TTT.CS2.Extensions;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace TTT.Shop;
|
||||
|
||||
@@ -30,6 +31,9 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
|
||||
private readonly IShop shop = provider.GetRequiredService<IShop>();
|
||||
|
||||
private readonly IMsgLocalizer localizer =
|
||||
provider.GetRequiredService<IMsgLocalizer>();
|
||||
|
||||
private IDisposable? rewardTimer, updateTimer;
|
||||
|
||||
private ShopConfig config
|
||||
@@ -67,7 +71,8 @@ public class PeriodicRewarder(IServiceProvider provider) : ITerrorModule {
|
||||
var position = count == 1 ? 1f : (float)(count - i - 1) / (count - 1);
|
||||
var rewardAmount = scaleRewardAmount(position, config.MinRewardAmount,
|
||||
config.MaxRewardAmount);
|
||||
shop.AddBalance(player, rewardAmount, "Exploration");
|
||||
shop.AddBalance(player, rewardAmount,
|
||||
localizer[ShopMsgs.SHOP_EXPLORATION]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public static class ShopMsgs {
|
||||
public static IMsg SHOP_INACTIVE => MsgFactory.Create(nameof(SHOP_INACTIVE));
|
||||
|
||||
public static IMsg CREDITS_NAME => MsgFactory.Create(nameof(CREDITS_NAME));
|
||||
|
||||
public static IMsg SHOP_EXPLORATION => MsgFactory.Create(nameof(SHOP_EXPLORATION));
|
||||
|
||||
public static IMsg SHOP_CANNOT_PURCHASE
|
||||
=> MsgFactory.Create(nameof(SHOP_CANNOT_PURCHASE));
|
||||
|
||||
@@ -2,6 +2,8 @@ SHOP_PREFIX: "{green}SHOP {grey}| "
|
||||
SHOP_INACTIVE: "%SHOP_PREFIX%The shop is currently closed."
|
||||
SHOP_ITEM_NOT_FOUND: "%SHOP_PREFIX%Could not find an item named \"{default}{0}{grey}\"."
|
||||
|
||||
SHOP_EXPLORATION: "Exploration"
|
||||
|
||||
SHOP_ITEM_DEAGLE: "One-Hit Revolver"
|
||||
SHOP_ITEM_DEAGLE_DESC: "If you hit an enemy, they will die instantly. Hitting a teammate will kill you instead"
|
||||
SHOP_ITEM_DEAGLE_HIT_FF: "%PREFIX%You hit a teammate!"
|
||||
|
||||
11
TTT/SpecialRound/Events/SpecialRoundEnableEvent.cs
Normal file
11
TTT/SpecialRound/Events/SpecialRoundEnableEvent.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using SpecialRoundAPI;
|
||||
|
||||
namespace SpecialRound.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a special round is enabled.
|
||||
/// Note that multiple special rounds may be enabled per round.
|
||||
/// </summary>
|
||||
/// <param name="round"></param>
|
||||
public class SpecialRoundEnableEvent(AbstractSpecialRound round)
|
||||
: SpecialRoundEvent(round);
|
||||
@@ -1,6 +0,0 @@
|
||||
using SpecialRoundAPI;
|
||||
|
||||
namespace SpecialRound.Events;
|
||||
|
||||
public class SpecialRoundStartEvent(AbstractSpecialRound round)
|
||||
: SpecialRoundEvent(round) { }
|
||||
44
TTT/SpecialRound/Rounds/LowGravRound.cs
Normal file
44
TTT/SpecialRound/Rounds/LowGravRound.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Modules.Cvars;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Game;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class LowGravRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider) {
|
||||
public override string Name => "Low Grav";
|
||||
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_LOWGRAV;
|
||||
public override SpecialRoundConfig Config => config;
|
||||
|
||||
private LowGravRoundConfig config
|
||||
=> Provider.GetService<IStorage<LowGravRoundConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new LowGravRoundConfig();
|
||||
|
||||
private int originalGravity = 800;
|
||||
|
||||
public override void ApplyRoundEffects() {
|
||||
var cvar = ConVar.Find("sv_gravity");
|
||||
if (cvar == null) return;
|
||||
|
||||
originalGravity = cvar.GetPrimitiveValue<int>();
|
||||
var newGravity = (int)(originalGravity * config.GravityMultiplier);
|
||||
Server.NextWorldUpdate(()
|
||||
=> Server.ExecuteCommand($"sv_gravity {newGravity}"));
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
|
||||
Server.NextWorldUpdate(()
|
||||
=> Server.ExecuteCommand($"sv_gravity {originalGravity}"));
|
||||
}
|
||||
}
|
||||
52
TTT/SpecialRound/Rounds/RichRound.cs
Normal file
52
TTT/SpecialRound/Rounds/RichRound.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using CounterStrikeSharp.API.Modules.Utils;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ShopAPI;
|
||||
using ShopAPI.Events;
|
||||
using SpecialRound.lang;
|
||||
using SpecialRoundAPI;
|
||||
using SpecialRoundAPI.Configs;
|
||||
using TTT.API.Events;
|
||||
using TTT.API.Storage;
|
||||
using TTT.Game.Events.Game;
|
||||
using TTT.Locale;
|
||||
|
||||
namespace SpecialRound.Rounds;
|
||||
|
||||
public class RichRound(IServiceProvider provider)
|
||||
: AbstractSpecialRound(provider) {
|
||||
public override string Name => "Rich";
|
||||
public override IMsg Description => RoundMsgs.SPECIAL_ROUND_RICH;
|
||||
public override SpecialRoundConfig Config => config;
|
||||
|
||||
private RichRoundConfig config
|
||||
=> Provider.GetService<IStorage<RichRoundConfig>>()
|
||||
?.Load()
|
||||
.GetAwaiter()
|
||||
.GetResult() ?? new RichRoundConfig();
|
||||
|
||||
public override void ApplyRoundEffects() { }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnBalanceChange(PlayerBalanceEvent ev) {
|
||||
if (!Tracker.ActiveRounds.Contains(this)) return;
|
||||
if (ev.Reason == "Round Start") {
|
||||
var newBal = (int)(ev.NewBalance * config.BonusCreditsMultiplier);
|
||||
ev.NewBalance = newBal;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.NewBalance <= ev.OldBalance) return;
|
||||
|
||||
var gain = ev.NewBalance - ev.OldBalance;
|
||||
gain = (int)(gain * config.AdditiveCreditsMultiplier);
|
||||
ev.NewBalance = ev.OldBalance + gain;
|
||||
}
|
||||
|
||||
public override bool ConflictsWith(AbstractSpecialRound other) {
|
||||
return other is VanillaRound;
|
||||
}
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) { }
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public class SpeedRound(IServiceProvider provider)
|
||||
public void OnDeath(PlayerDeathEvent ev) {
|
||||
var game = games.ActiveGame;
|
||||
if (game == null) return;
|
||||
if (Tracker.CurrentRound != this) return;
|
||||
if (Tracker.ActiveRounds.Contains(this)) return;
|
||||
|
||||
var victimRoles = roles.GetRoles(ev.Victim);
|
||||
if (!victimRoles.Any(r => r is InnocentRole)) return;
|
||||
|
||||
@@ -35,10 +35,14 @@ public class VanillaRound(IServiceProvider provider)
|
||||
|
||||
public override void OnGameState(GameStateUpdateEvent ev) { }
|
||||
|
||||
public override bool ConflictsWith(AbstractSpecialRound other) {
|
||||
return other is RichRound;
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler(Priority = Priority.HIGH)]
|
||||
public void OnPurchase(PlayerPurchaseItemEvent ev) {
|
||||
if (Tracker.CurrentRound != this) return;
|
||||
if (Tracker.ActiveRounds.Contains(this)) return;
|
||||
ev.IsCanceled = true;
|
||||
|
||||
messenger.Message(ev.Player, locale[RoundMsgs.VANILLA_ROUND_REMINDER]);
|
||||
|
||||
@@ -11,11 +11,13 @@ public static class SpecialRoundCollection {
|
||||
services.AddModBehavior<ISpecialRoundTracker, SpecialRoundTracker>();
|
||||
services.AddModBehavior<SpecialRoundSoundNotifier>();
|
||||
|
||||
services.AddModBehavior<SpeedRound>();
|
||||
services.AddModBehavior<BhopRound>();
|
||||
services.AddModBehavior<VanillaRound>();
|
||||
services.AddModBehavior<SuppressedRound>();
|
||||
services.AddModBehavior<SilentRound>();
|
||||
services.AddModBehavior<LowGravRound>();
|
||||
services.AddModBehavior<PistolRound>();
|
||||
services.AddModBehavior<RichRound>();
|
||||
services.AddModBehavior<SilentRound>();
|
||||
services.AddModBehavior<SpeedRound>();
|
||||
services.AddModBehavior<SuppressedRound>();
|
||||
services.AddModBehavior<VanillaRound>();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public class SpecialRoundSoundNotifier(IServiceProvider provider)
|
||||
: BaseListener(provider) {
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnSpecialRoundStart(SpecialRoundStartEvent ev) {
|
||||
public void OnSpecialRoundStart(SpecialRoundEnableEvent ev) {
|
||||
foreach (var player in Utilities.GetPlayers())
|
||||
player.EmitSound("UI.XP.Star.Spend", null, 0.2f);
|
||||
}
|
||||
|
||||
@@ -30,20 +30,21 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
plugin?.RegisterListener<Listeners.OnMapStart>(onMapChange);
|
||||
}
|
||||
|
||||
public AbstractSpecialRound?
|
||||
TryStartSpecialRound(AbstractSpecialRound? round) {
|
||||
round ??= getSpecialRound();
|
||||
public List<AbstractSpecialRound> TryStartSpecialRound(
|
||||
List<AbstractSpecialRound>? rounds = null) {
|
||||
rounds ??= getSpecialRounds();
|
||||
|
||||
var roundStart = new SpecialRoundStartEvent(round);
|
||||
Bus.Dispatch(roundStart);
|
||||
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(rounds)]);
|
||||
|
||||
Messenger.MessageAll(Locale[RoundMsgs.SPECIAL_ROUND_STARTED(round)]);
|
||||
Messenger.MessageAll(Locale[round.Description]);
|
||||
foreach (var round in rounds) {
|
||||
var roundStart = new SpecialRoundEnableEvent(round);
|
||||
Bus.Dispatch(roundStart);
|
||||
Messenger.MessageAll(Locale[round.Description]);
|
||||
round.ApplyRoundEffects();
|
||||
}
|
||||
|
||||
round.ApplyRoundEffects();
|
||||
tracker.CurrentRound = round;
|
||||
tracker.RoundsSinceLastSpecial = 0;
|
||||
return round;
|
||||
return rounds;
|
||||
}
|
||||
|
||||
private void onMapChange(string mapName) { roundsSinceMapChange = 0; }
|
||||
@@ -62,16 +63,32 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
if (roundsSinceMapChange < config.MinRoundsAfterMapChange) return;
|
||||
if (Random.Shared.NextSingle() > config.SpecialRoundChance) return;
|
||||
|
||||
var specialRound = getSpecialRound();
|
||||
var specialRound = getSpecialRounds();
|
||||
|
||||
TryStartSpecialRound(specialRound);
|
||||
}
|
||||
|
||||
private AbstractSpecialRound getSpecialRound() {
|
||||
private List<AbstractSpecialRound> getSpecialRounds() {
|
||||
var selectedRounds = new List<AbstractSpecialRound>();
|
||||
|
||||
do {
|
||||
var round = pickWeightedRound(selectedRounds);
|
||||
if (round == null) break;
|
||||
selectedRounds.Add(round);
|
||||
} while (config.MultiRoundChance > Random.Shared.NextSingle());
|
||||
|
||||
return selectedRounds;
|
||||
}
|
||||
|
||||
private AbstractSpecialRound? pickWeightedRound(
|
||||
List<AbstractSpecialRound> exclude) {
|
||||
var rounds = Provider.GetServices<ITerrorModule>()
|
||||
.OfType<AbstractSpecialRound>()
|
||||
.Where(r => r.Config.Weight > 0)
|
||||
.Where(r => r.Config.Weight > 0 && !exclude.Contains(r))
|
||||
.Where(r
|
||||
=> !exclude.Any(er => er.ConflictsWith(r) && !r.ConflictsWith(er)))
|
||||
.ToList();
|
||||
if (rounds.Count == 0) return null;
|
||||
var totalWeight = rounds.Sum(r => r.Config.Weight);
|
||||
var roll = Random.Shared.NextDouble() * totalWeight;
|
||||
foreach (var round in rounds) {
|
||||
@@ -79,7 +96,6 @@ public class SpecialRoundStarter(IServiceProvider provider)
|
||||
if (roll <= 0) return round;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Failed to select a special round. This should never happen.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace SpecialRound;
|
||||
public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
|
||||
IListener {
|
||||
public AbstractSpecialRound? CurrentRound { get; set; }
|
||||
public List<AbstractSpecialRound> ActiveRounds { get; } = new();
|
||||
public int RoundsSinceLastSpecial { get; set; }
|
||||
public void Dispose() { }
|
||||
public void Start() { }
|
||||
@@ -19,5 +20,6 @@ public class SpecialRoundTracker : ISpecialRoundTracker, ITerrorModule,
|
||||
public void OnRoundEnd(GameStateUpdateEvent ev) {
|
||||
if (ev.NewState != State.FINISHED) return;
|
||||
CurrentRound = null;
|
||||
ActiveRounds.Clear();
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,11 @@ public record SpecialRoundsConfig {
|
||||
public int MinPlayersForSpecial { get; init; } = 5;
|
||||
public int MinRoundsAfterMapChange { get; init; } = 2;
|
||||
public float SpecialRoundChance { get; init; } = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// If a special round is started, the chance that another special round
|
||||
/// will start in conjunction with it. This check is run until it fails,
|
||||
/// or we run out of special rounds to start.
|
||||
/// </summary>
|
||||
public float MultiRoundChance { get; init; } = 0.33f;
|
||||
}
|
||||
@@ -25,7 +25,16 @@ public class RoundMsgs {
|
||||
public static IMsg SPECIAL_ROUND_PISTOL
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_PISTOL));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_STARTED(AbstractSpecialRound round) {
|
||||
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), round.Name);
|
||||
public static IMsg SPECIAL_ROUND_RICH
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_RICH));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_LOWGRAV
|
||||
=> MsgFactory.Create(nameof(SPECIAL_ROUND_LOWGRAV));
|
||||
|
||||
public static IMsg SPECIAL_ROUND_STARTED(List<AbstractSpecialRound> round) {
|
||||
var roundNames = round.Count == 1 ?
|
||||
round[0].Name :
|
||||
string.Join(", ", round.Select(r => r.Name));
|
||||
return MsgFactory.Create(nameof(SPECIAL_ROUND_STARTED), roundNames);
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,6 @@ SPECIAL_ROUND_VANILLA: " {green}VANILLA{grey}: The shop has been disabled!"
|
||||
SPECIAL_ROUND_SUPPRESSED: " {grey}SUPPRESSED{grey}: All pistols are silent!"
|
||||
SPECIAL_ROUND_SILENT: " {grey}SILENT{grey}: All players are muted!"
|
||||
SPECIAL_ROUND_PISTOL: " {blue}PISTOL{grey}: You can only use pistols this round!"
|
||||
SPECIAL_ROUND_RICH: " {gold}RICH{grey}: All players start with extra credits!"
|
||||
SPECIAL_ROUND_LOWGRAV: " {lightblue}LOW GRAVITY{grey}: Players can jump higher and fall slower!"
|
||||
VANILLA_ROUND_REMINDER: "%SHOP_PREFIX%This is a {purple}Vanilla{grey} round. The shop is disabled."
|
||||
@@ -19,6 +19,8 @@ public abstract class AbstractSpecialRound(IServiceProvider provider)
|
||||
|
||||
public abstract void ApplyRoundEffects();
|
||||
|
||||
public virtual bool ConflictsWith(AbstractSpecialRound _) { return false; }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public abstract void OnGameState(GameStateUpdateEvent ev);
|
||||
|
||||
6
TTT/SpecialRoundAPI/Configs/LowGravRoundConfig.cs
Normal file
6
TTT/SpecialRoundAPI/Configs/LowGravRoundConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record LowGravRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.6f;
|
||||
public float GravityMultiplier { get; init; } = 0.5f;
|
||||
}
|
||||
7
TTT/SpecialRoundAPI/Configs/RichRoundConfig.cs
Normal file
7
TTT/SpecialRoundAPI/Configs/RichRoundConfig.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace SpecialRoundAPI.Configs;
|
||||
|
||||
public record RichRoundConfig : SpecialRoundConfig {
|
||||
public override float Weight { get; init; } = 0.75f;
|
||||
public float BonusCreditsMultiplier { get; init; } = 2.0f;
|
||||
public float AdditiveCreditsMultiplier { get; init; } = 3.0f;
|
||||
}
|
||||
@@ -8,6 +8,6 @@ public interface ISpecialRoundStarter {
|
||||
/// </summary>
|
||||
/// <param name="round"></param>
|
||||
/// <returns></returns>
|
||||
public AbstractSpecialRound?
|
||||
TryStartSpecialRound(AbstractSpecialRound? round);
|
||||
public List<AbstractSpecialRound>? TryStartSpecialRound(
|
||||
List<AbstractSpecialRound>? round = null);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace SpecialRoundAPI;
|
||||
|
||||
public interface ISpecialRoundTracker {
|
||||
public AbstractSpecialRound? CurrentRound { get; set; }
|
||||
public List<AbstractSpecialRound> ActiveRounds { get; }
|
||||
|
||||
public int RoundsSinceLastSpecial { get; set; }
|
||||
}
|
||||
@@ -23,7 +23,7 @@ public class SpecialRoundListener(IServiceProvider provider)
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
public void OnRoundStart(SpecialRoundStartEvent ev) { round = ev.Round; }
|
||||
public void OnRoundStart(SpecialRoundEnableEvent ev) { round = ev.Round; }
|
||||
|
||||
[UsedImplicitly]
|
||||
[EventHandler]
|
||||
|
||||
Reference in New Issue
Block a user