Multiantag Gamemode (#37783)

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Southbridge <7013162+southbridge-fur@users.noreply.github.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
This commit is contained in:
Nemanja
2025-08-15 10:06:51 -04:00
committed by GitHub
parent b2c505df6a
commit f23e8c2861
19 changed files with 787 additions and 5 deletions

View File

@@ -34,6 +34,8 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public TimeSpan StartTime { get; private set; } [ViewVariables] public TimeSpan StartTime { get; private set; }
[ViewVariables] public new bool Paused { get; private set; } [ViewVariables] public new bool Paused { get; private set; }
public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => new List<(TimeSpan, string)>();
[ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailable => _jobsAvailable; [ViewVariables] public IReadOnlyDictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailable => _jobsAvailable;
[ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames; [ViewVariables] public IReadOnlyDictionary<NetEntity, string> StationNames => _stationNames;

View File

@@ -0,0 +1,103 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking.Rules;
using Content.Shared.Administration;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
namespace Content.Server.GameTicking.Commands;
[ToolshedCommand, AdminCommand(AdminFlags.Round)]
public sealed class DynamicRuleCommand : ToolshedCommand
{
private DynamicRuleSystem? _dynamicRuleSystem;
[CommandImplementation("list")]
public IEnumerable<EntityUid> List()
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.GetDynamicRules();
}
[CommandImplementation("get")]
public EntityUid Get()
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.GetDynamicRules().FirstOrDefault();
}
[CommandImplementation("budget")]
public IEnumerable<float?> Budget([PipedArgument] IEnumerable<EntityUid> input)
=> input.Select(Budget);
[CommandImplementation("budget")]
public float? Budget([PipedArgument] EntityUid input)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.GetRuleBudget(input);
}
[CommandImplementation("adjust")]
public IEnumerable<float?> Adjust([PipedArgument] IEnumerable<EntityUid> input, float value)
=> input.Select(i => Adjust(i,value));
[CommandImplementation("adjust")]
public float? Adjust([PipedArgument] EntityUid input, float value)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.AdjustBudget(input, value);
}
[CommandImplementation("set")]
public IEnumerable<float?> Set([PipedArgument] IEnumerable<EntityUid> input, float value)
=> input.Select(i => Set(i,value));
[CommandImplementation("set")]
public float? Set([PipedArgument] EntityUid input, float value)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.SetBudget(input, value);
}
[CommandImplementation("dryrun")]
public IEnumerable<IEnumerable<EntProtoId>> DryRun([PipedArgument] IEnumerable<EntityUid> input)
=> input.Select(DryRun);
[CommandImplementation("dryrun")]
public IEnumerable<EntProtoId> DryRun([PipedArgument] EntityUid input)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.DryRun(input);
}
[CommandImplementation("executenow")]
public IEnumerable<IEnumerable<EntityUid>> ExecuteNow([PipedArgument] IEnumerable<EntityUid> input)
=> input.Select(ExecuteNow);
[CommandImplementation("executenow")]
public IEnumerable<EntityUid> ExecuteNow([PipedArgument] EntityUid input)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.ExecuteNow(input);
}
[CommandImplementation("rules")]
public IEnumerable<IEnumerable<EntityUid>> Rules([PipedArgument] IEnumerable<EntityUid> input)
=> input.Select(Rules);
[CommandImplementation("rules")]
public IEnumerable<EntityUid> Rules([PipedArgument] EntityUid input)
{
_dynamicRuleSystem ??= GetSys<DynamicRuleSystem>();
return _dynamicRuleSystem.Rules(input);
}
}

View File

@@ -21,7 +21,7 @@ public sealed partial class GameTicker
/// A list storing the start times of all game rules that have been started this round. /// A list storing the start times of all game rules that have been started this round.
/// Game rules can be started and stopped at any time, including midround. /// Game rules can be started and stopped at any time, including midround.
/// </summary> /// </summary>
public IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules; public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules;
private void InitializeGameRules() private void InitializeGameRules()
{ {

View File

@@ -0,0 +1,195 @@
using System.Diagnostics;
using Content.Server.Administration.Logs;
using Content.Server.RoundEnd;
using Content.Shared.Database;
using Content.Shared.EntityTable;
using Content.Shared.EntityTable.Conditions;
using Content.Shared.GameTicking.Components;
using Content.Shared.GameTicking.Rules;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class DynamicRuleSystem : GameRuleSystem<DynamicRuleComponent>
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Added(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, component, gameRule, args);
component.Budget = _random.Next(component.StartingBudgetMin, component.StartingBudgetMax);;
component.NextRuleTime = Timing.CurTime + _random.Next(component.MinRuleInterval, component.MaxRuleInterval);
}
protected override void Started(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// Since we don't know how long until this rule is activated, we need to
// set the last budget update to now so it doesn't immediately give the component a bunch of points.
component.LastBudgetUpdate = Timing.CurTime;
Execute((uid, component));
}
protected override void Ended(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
base.Ended(uid, component, gameRule, args);
foreach (var rule in component.Rules)
{
GameTicker.EndGameRule(rule);
}
}
protected override void ActiveTick(EntityUid uid, DynamicRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (Timing.CurTime < component.NextRuleTime)
return;
// don't spawn antags during evac
if (_roundEnd.IsRoundEndRequested())
return;
Execute((uid, component));
}
/// <summary>
/// Generates and returns a list of randomly selected,
/// valid rules to spawn based on <see cref="DynamicRuleComponent.Table"/>.
/// </summary>
private IEnumerable<EntProtoId> GetRuleSpawns(Entity<DynamicRuleComponent> entity)
{
UpdateBudget((entity.Owner, entity.Comp));
var ctx = new EntityTableContext(new Dictionary<string, object>
{
{ HasBudgetCondition.BudgetContextKey, entity.Comp.Budget },
});
return _entityTable.GetSpawns(entity.Comp.Table, ctx: ctx);
}
/// <summary>
/// Updates the budget of the provided dynamic rule component based on the amount of time since the last update
/// multiplied by the <see cref="DynamicRuleComponent.BudgetPerSecond"/> value.
/// </summary>
private void UpdateBudget(Entity<DynamicRuleComponent> entity)
{
var duration = (float) (Timing.CurTime - entity.Comp.LastBudgetUpdate).TotalSeconds;
entity.Comp.Budget += duration * entity.Comp.BudgetPerSecond;
entity.Comp.LastBudgetUpdate = Timing.CurTime;
}
/// <summary>
/// Executes this rule, generating new dynamic rules and starting them.
/// </summary>
/// <returns>
/// Returns a list of the rules that were executed.
/// </returns>
private List<EntityUid> Execute(Entity<DynamicRuleComponent> entity)
{
entity.Comp.NextRuleTime =
Timing.CurTime + _random.Next(entity.Comp.MinRuleInterval, entity.Comp.MaxRuleInterval);
var executedRules = new List<EntityUid>();
foreach (var rule in GetRuleSpawns(entity))
{
var res = GameTicker.StartGameRule(rule, out var ruleUid);
Debug.Assert(res);
executedRules.Add(ruleUid);
if (TryComp<DynamicRuleCostComponent>(ruleUid, out var cost))
{
entity.Comp.Budget -= cost.Cost;
_adminLog.Add(LogType.EventRan, LogImpact.High, $"{ToPrettyString(entity)} ran rule {ToPrettyString(ruleUid)} with cost {cost.Cost} on budget {entity.Comp.Budget}.");
}
else
{
_adminLog.Add(LogType.EventRan, LogImpact.High, $"{ToPrettyString(entity)} ran rule {ToPrettyString(ruleUid)} which had no cost.");
}
}
entity.Comp.Rules.AddRange(executedRules);
return executedRules;
}
#region Command Methods
public List<EntityUid> GetDynamicRules()
{
var rules = new List<EntityUid>();
var query = EntityQueryEnumerator<DynamicRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var comp))
{
if (!GameTicker.IsGameRuleActive(uid, comp))
continue;
rules.Add(uid);
}
return rules;
}
public float? GetRuleBudget(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return null;
UpdateBudget((entity.Owner, entity.Comp));
return entity.Comp.Budget;
}
public float? AdjustBudget(Entity<DynamicRuleComponent?> entity, float amount)
{
if (!Resolve(entity, ref entity.Comp))
return null;
UpdateBudget((entity.Owner, entity.Comp));
entity.Comp.Budget += amount;
return entity.Comp.Budget;
}
public float? SetBudget(Entity<DynamicRuleComponent?> entity, float amount)
{
if (!Resolve(entity, ref entity.Comp))
return null;
entity.Comp.LastBudgetUpdate = Timing.CurTime;
entity.Comp.Budget = amount;
return entity.Comp.Budget;
}
public IEnumerable<EntProtoId> DryRun(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntProtoId>();
return GetRuleSpawns((entity.Owner, entity.Comp));
}
public IEnumerable<EntityUid> ExecuteNow(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntityUid>();
return Execute((entity.Owner, entity.Comp));
}
public IEnumerable<EntityUid> Rules(Entity<DynamicRuleComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return new List<EntityUid>();
return entity.Comp.Rules;
}
#endregion
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking.Rules;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that only succeeds if a table supplies a sufficient "cost" to a given
/// </summary>
public sealed partial class HasBudgetCondition : EntityTableCondition
{
public const string BudgetContextKey = "Budget";
/// <summary>
/// Used for determining the cost for the budget.
/// If null, attempts to fetch the cost from the attached selector.
/// </summary>
[DataField]
public int? CostOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
if (!ctx.TryGetData<float>(BudgetContextKey, out var budget))
return false;
int cost;
if (CostOverride != null)
{
cost = CostOverride.Value;
}
else
{
if (root is not EntSelector entSelector)
return false;
if (!proto.Index(entSelector.Id).TryGetComponent(out DynamicRuleCostComponent? costComponent, entMan.ComponentFactory))
{
var log = Logger.GetSawmill("HasBudgetCondition");
log.Error($"Rule {entSelector.Id} does not have a DynamicRuleCostComponent.");
return false;
}
cost = costComponent.Cost;
}
return budget >= cost;
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that succeeds only when the specified gamerule has been run under a certain amount of times
/// </summary>
/// <remarks>
/// This is meant to be attached directly to EntSelector. If it is not, then you'll need to specify what rule
/// is being used inside RuleOverride.
/// </remarks>
public sealed partial class MaxRuleOccurenceCondition : EntityTableCondition
{
/// <summary>
/// The maximum amount of times this rule can have already be run.
/// </summary>
[DataField]
public int Max = 1;
/// <summary>
/// The rule that is being checked for occurrences.
/// If null, it will use the value on the attached selector.
/// </summary>
[DataField]
public EntProtoId? RuleOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
string rule;
if (RuleOverride is { } ruleOverride)
{
rule = ruleOverride;
}
else
{
rule = root is EntSelector entSelector
? entSelector.Id
: string.Empty;
}
if (rule == string.Empty)
return false;
var gameTicker = entMan.System<SharedGameTicker>();
return gameTicker.AllPreviousGameRules.Count(p => p.Item2 == rule) < Max;
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
public sealed partial class ReoccurrenceDelayCondition : EntityTableCondition
{
/// <summary>
/// The maximum amount of times this rule can have already be run.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.Zero;
/// <summary>
/// The rule that is being checked for occurrences.
/// If null, it will use the value on the attached selector.
/// </summary>
[DataField]
public EntProtoId? RuleOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
string rule;
if (RuleOverride is { } ruleOverride)
{
rule = ruleOverride;
}
else
{
rule = root is EntSelector entSelector
? entSelector.Id
: string.Empty;
}
if (rule == string.Empty)
return false;
var gameTicker = entMan.System<SharedGameTicker>();
return gameTicker.AllPreviousGameRules.Any(
p => p.Item2 == rule && p.Item1 + Delay <= gameTicker.RoundDuration());
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that passes only if the current round time falls between the minimum and maximum time values.
/// </summary>
public sealed partial class RoundDurationCondition : EntityTableCondition
{
/// <summary>
/// Minimum time the round must have gone on for this condition to pass.
/// </summary>
[DataField]
public TimeSpan Min = TimeSpan.Zero;
/// <summary>
/// Maximum amount of time the round could go on for this condition to pass.
/// </summary>
[DataField]
public TimeSpan Max = TimeSpan.MaxValue;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
var gameTicker = entMan.System<SharedGameTicker>();
var duration = gameTicker.RoundDuration();
return duration >= Min && duration <= Max;
}
}

View File

@@ -26,6 +26,9 @@ public sealed partial class GroupSelector : EntityTableSelector
children.Add(child, child.Weight); children.Add(child, child.Weight);
} }
if (children.Count == 0)
return Array.Empty<EntProtoId>();
var pick = SharedRandomExtensions.Pick(children, rand); var pick = SharedRandomExtensions.Pick(children, rand);
return pick.GetSpawns(rand, entMan, proto, ctx); return pick.GetSpawns(rand, entMan, proto, ctx);

View File

@@ -0,0 +1,71 @@
using Content.Shared.EntityTable.EntitySelectors;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.GameTicking.Rules;
/// <summary>
/// Gamerule the spawns multiple antags at intervals based on a budget
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class DynamicRuleComponent : Component
{
/// <summary>
/// The total budget for antags.
/// </summary>
[DataField]
public float Budget;
/// <summary>
/// The last time budget was updated.
/// </summary>
[DataField]
public TimeSpan LastBudgetUpdate;
/// <summary>
/// The amount of budget accumulated every second.
/// </summary>
[DataField]
public float BudgetPerSecond = 0.1f;
/// <summary>
/// The minimum or lower bound for budgets to start at.
/// </summary>
[DataField]
public int StartingBudgetMin = 200;
/// <summary>
/// The maximum or upper bound for budgets to start at.
/// </summary>
[DataField]
public int StartingBudgetMax = 350;
/// <summary>
/// The time at which the next rule will start
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextRuleTime;
/// <summary>
/// Minimum delay between rules
/// </summary>
[DataField]
public TimeSpan MinRuleInterval = TimeSpan.FromMinutes(10);
/// <summary>
/// Maximum delay between rules
/// </summary>
[DataField]
public TimeSpan MaxRuleInterval = TimeSpan.FromMinutes(30);
/// <summary>
/// A table of rules that are picked from.
/// </summary>
[DataField]
public EntityTableSelector Table = new NoneSelector();
/// <summary>
/// The rules that have been spawned
/// </summary>
[DataField]
public List<EntityUid> Rules = new();
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.GameTicking.Rules;
/// <summary>
/// Component that tracks how much a rule "costs" for Dynamic
/// </summary>
[RegisterComponent]
public sealed partial class DynamicRuleCostComponent : Component
{
/// <summary>
/// The amount of budget a rule takes up
/// </summary>
[DataField(required: true)]
public int Cost;
}

View File

@@ -15,6 +15,12 @@ namespace Content.Shared.GameTicking
[Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
/// <summary>
/// A list storing the start times of all game rules that have been started this round.
/// Game rules can be started and stopped at any time, including midround.
/// </summary>
public abstract IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules { get; }
// See ideally these would be pulled from the job definition or something. // See ideally these would be pulled from the job definition or something.
// But this is easier, and at least it isn't hardcoded. // But this is easier, and at least it isn't hardcoded.
//TODO: Move these, they really belong in StationJobsSystem or a cvar. //TODO: Move these, they really belong in StationJobsSystem or a cvar.

View File

@@ -106,3 +106,19 @@ command-description-scale-multiplyvector =
Multiply an entity's sprite size with a certain 2d vector (without changing its fixture). Multiply an entity's sprite size with a certain 2d vector (without changing its fixture).
command-description-scale-multiplywithfixture = command-description-scale-multiplywithfixture =
Multiply an entity's sprite size with a certain factor (including its fixture). Multiply an entity's sprite size with a certain factor (including its fixture).
command-description-dynamicrule-list =
Lists all currently active dynamic rules, usually this is just one.
command-description-dynamicrule-get =
Gets the currently active dynamic rule.
command-description-dynamicrule-budget =
Gets the current budget of the piped dynamic rule(s).
command-description-dynamicrule-adjust =
Adjusts the budget of the piped dynamic rule(s) by the specified amount.
command-description-dynamicrule-set =
Sets the budget of the piped dynamic rule(s) to the specified amount.
command-description-dynamicrule-dryrun =
Returns a list of rules that could be activated if the rule ran at this moment with all current context. This is not a complete list of every single rule that could be run, just a sample of the current valid ones.
command-description-dynamicrule-executenow =
Executes the piped dynamic rule as if it had reached its regular update time.
command-description-dynamicrule-rules =
Gets a list of all the rules spawned by the piped dynamic rule.

View File

@@ -1,2 +1,5 @@
secret-title = Secret secret-title = Secret
secret-description = It's a secret to everyone. The threats you encounter are randomized. secret-description = It's a secret to everyone. The threats you encounter are randomized.
dynamic-title = Dynamic
dynamic-description = No one knows what's coming. You can encounter any number of threats.

View File

@@ -0,0 +1,112 @@
- type: entity
parent: BaseGameRule
id: DynamicRule
components:
- type: GameRule
minPlayers: 5 # <5 is greenshift hours, buddy.
- type: DynamicRule
startingBudgetMin: 200
startingBudgetMax: 350
table: !type:AllSelector
children:
# Roundstart Major Rules
- !type:GroupSelector
conditions:
- !type:RoundDurationCondition
max: 1
children:
- id: Traitor
weight: 60
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- id: Nukeops
weight: 25
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:PlayerCountCondition
min: 20
- id: Revolutionary
weight: 5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- id: Zombie
weight: 5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:PlayerCountCondition
min: 20
- id: Wizard
weight: 5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:PlayerCountCondition
min: 10
# Roundstart Minor Rules
- !type:GroupSelector
conditions:
- !type:RoundDurationCondition
max: 1
children:
- id: Thief
prob: 0.5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
# Midround rules
- !type:GroupSelector
conditions:
- !type:RoundDurationCondition
min: 300 # minimum 5 minutes
children:
- id: SleeperAgents
weight: 15
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:RoundDurationCondition
min: 900 # 15 minutes
- id: DragonSpawn
weight: 15
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:RoundDurationCondition
min: 900 # 15 minutes
- id: NinjaSpawn
weight: 20
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:RoundDurationCondition
min: 900 # 15 minutes
- id: ParadoxCloneSpawn
weight: 25
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
max: 2
- !type:RoundDurationCondition
min: 600 # 10 minutes
- id: ZombieOutbreak
weight: 2.5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:PlayerCountCondition
min: 20
- !type:RoundDurationCondition
min: 2700 # 45 minutes
- id: LoneOpsSpawn
weight: 5
conditions:
- !type:HasBudgetCondition
- !type:MaxRuleOccurenceCondition
- !type:PlayerCountCondition
min: 20
- !type:RoundDurationCondition
min: 2100 # 35 minutes

View File

@@ -30,15 +30,20 @@
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
children: children:
- id: ClosetSkeleton - id: ClosetSkeleton
- id: DragonSpawn
- id: KingRatMigration - id: KingRatMigration
- id: RevenantSpawn
- id: DerelictCyborgSpawn
- type: entityTable
id: ModerateAntagEventsTable
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
children:
- id: DragonSpawn
- id: NinjaSpawn - id: NinjaSpawn
- id: ParadoxCloneSpawn - id: ParadoxCloneSpawn
- id: RevenantSpawn
- id: SleeperAgents - id: SleeperAgents
- id: ZombieOutbreak - id: ZombieOutbreak
- id: LoneOpsSpawn - id: LoneOpsSpawn
- id: DerelictCyborgSpawn
- id: WizardSpawn - id: WizardSpawn
- type: entity - type: entity
@@ -183,6 +188,8 @@
pickPlayer: false pickPlayer: false
mindRoles: mindRoles:
- MindRoleDragon - MindRoleDragon
- type: DynamicRuleCost
cost: 75
- type: entity - type: entity
parent: BaseGameRule parent: BaseGameRule
@@ -233,6 +240,8 @@
nameFormat: name-format-ninja nameFormat: name-format-ninja
mindRoles: mindRoles:
- MindRoleNinja - MindRoleNinja
- type: DynamicRuleCost
cost: 75
- type: entity - type: entity
parent: BaseGameRule parent: BaseGameRule
@@ -269,6 +278,8 @@
sound: /Audio/Misc/paradox_clone_greeting.ogg sound: /Audio/Misc/paradox_clone_greeting.ogg
mindRoles: mindRoles:
- MindRoleParadoxClone - MindRoleParadoxClone
- type: DynamicRuleCost
cost: 50
- type: entity - type: entity
parent: BaseGameRule parent: BaseGameRule
@@ -520,6 +531,8 @@
- type: InitialInfected - type: InitialInfected
mindRoles: mindRoles:
- MindRoleInitialInfected - MindRoleInitialInfected
- type: DynamicRuleCost
cost: 200
- type: entity - type: entity
parent: BaseNukeopsRule parent: BaseNukeopsRule
@@ -556,6 +569,8 @@
- Syndicate - Syndicate
mindRoles: mindRoles:
- MindRoleNukeops - MindRoleNukeops
- type: DynamicRuleCost
cost: 75
- type: entity - type: entity
parent: BaseTraitorRule parent: BaseTraitorRule

View File

@@ -173,6 +173,8 @@
- Syndicate - Syndicate
mindRoles: mindRoles:
- MindRoleNukeops - MindRoleNukeops
- type: DynamicRuleCost
cost: 200
- type: entity - type: entity
abstract: true abstract: true
@@ -188,6 +190,8 @@
maxDifficulty: 5 maxDifficulty: 5
- type: AntagSelection - type: AntagSelection
agentName: traitor-round-end-agent-name agentName: traitor-round-end-agent-name
- type: DynamicRuleCost
cost: 100
- type: entity - type: entity
parent: BaseTraitorRule parent: BaseTraitorRule
@@ -207,7 +211,7 @@
blacklist: blacklist:
components: components:
- AntagImmune - AntagImmune
lateJoinAdditional: true lateJoinAdditional: false
mindRoles: mindRoles:
- MindRoleTraitor - MindRoleTraitor
@@ -278,6 +282,8 @@
- type: HeadRevolutionary - type: HeadRevolutionary
mindRoles: mindRoles:
- MindRoleHeadRevolutionary - MindRoleHeadRevolutionary
- type: DynamicRuleCost
cost: 200
- type: entity - type: entity
id: Sandbox id: Sandbox
@@ -341,6 +347,8 @@
nameFormat: name-format-wizard nameFormat: name-format-wizard
mindRoles: mindRoles:
- MindRoleWizard - MindRoleWizard
- type: DynamicRuleCost
cost: 150
- type: entity - type: entity
id: Zombie id: Zombie
@@ -373,6 +381,8 @@
- type: InitialInfected - type: InitialInfected
mindRoles: mindRoles:
- MindRoleInitialInfected - MindRoleInitialInfected
- type: DynamicRuleCost
cost: 200
# This rule makes the chosen players unable to get other antag rules, as a way to prevent metagaming job rolls. # This rule makes the chosen players unable to get other antag rules, as a way to prevent metagaming job rolls.
# Put this before antags assigned to station jobs, but after non-job antags (NukeOps/Wiz). # Put this before antags assigned to station jobs, but after non-job antags (NukeOps/Wiz).
@@ -400,6 +410,8 @@
tableId: BasicCalmEventsTable tableId: BasicCalmEventsTable
- !type:NestedSelector - !type:NestedSelector
tableId: BasicAntagEventsTable tableId: BasicAntagEventsTable
- !type:NestedSelector
tableId: ModerateAntagEventsTable
- !type:NestedSelector - !type:NestedSelector
tableId: CargoGiftsTable tableId: CargoGiftsTable
- !type:NestedSelector - !type:NestedSelector
@@ -407,6 +419,21 @@
- !type:NestedSelector - !type:NestedSelector
tableId: SpicyPestEventsTable tableId: SpicyPestEventsTable
- type: entityTable
id: DynamicGameRulesTable
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
children:
- !type:NestedSelector
tableId: BasicCalmEventsTable
- !type:NestedSelector
tableId: BasicAntagEventsTable
- !type:NestedSelector
tableId: CargoGiftsTable
- !type:NestedSelector
tableId: CalmPestEventsTable
- !type:NestedSelector
tableId: SpicyPestEventsTable
- type: entityTable - type: entityTable
id: SpaceTrafficControlTable id: SpaceTrafficControlTable
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
@@ -426,6 +453,14 @@
scheduledGameRules: !type:NestedSelector scheduledGameRules: !type:NestedSelector
tableId: BasicGameRulesTable tableId: BasicGameRulesTable
- type: entity
id: DynamicStationEventScheduler # this isn't the dynamic mode, but rather the station event scheduler used for dynamic
parent: BaseGameRule
components:
- type: BasicStationEventScheduler
scheduledGameRules: !type:NestedSelector
tableId: DynamicGameRulesTable
- type: entity - type: entity
id: RampingStationEventScheduler id: RampingStationEventScheduler
parent: BaseGameRule parent: BaseGameRule

View File

@@ -34,6 +34,8 @@
- MindRoleThief - MindRoleThief
briefing: briefing:
sound: "/Audio/Misc/thief_greeting.ogg" sound: "/Audio/Misc/thief_greeting.ogg"
- type: DynamicRuleCost
cost: 75
# Needs testing # Needs testing
- type: entity - type: entity

View File

@@ -96,6 +96,23 @@
- SpaceTrafficControlFriendlyEventScheduler - SpaceTrafficControlFriendlyEventScheduler
- BasicRoundstartVariation - BasicRoundstartVariation
- type: gamePreset
id: Dynamic
alias:
- dynamic
- multiantag
- director
name: dynamic-title
showInVote: true
description: dynamic-description
rules:
- DynamicRule
- DummyNonAntag
- DynamicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation
- type: gamePreset - type: gamePreset
id: Secret id: Secret
alias: alias: