[ViewVariables] public TimeSpan StartTime { 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, string> StationNames => _stationNames;
--- /dev/null
+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);
+ }
+}
+
/// 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 IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules;
+ public override IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules => _allPreviousGameRules;
private void InitializeGameRules()
{
--- /dev/null
+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
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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;
+ }
+}
children.Add(child, child.Weight);
}
+ if (children.Count == 0)
+ return Array.Empty<EntProtoId>();
+
var pick = SharedRandomExtensions.Pick(children, rand);
return pick.GetSpawns(rand, entMan, proto, ctx);
--- /dev/null
+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();
+}
--- /dev/null
+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;
+}
[Dependency] private readonly IReplayRecordingManager _replay = 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.
// But this is easier, and at least it isn't hardcoded.
//TODO: Move these, they really belong in StationJobsSystem or a cvar.
Multiply an entity's sprite size with a certain 2d vector (without changing its fixture).
command-description-scale-multiplywithfixture =
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.
secret-title = Secret
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.
--- /dev/null
+- 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
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
children:
- id: ClosetSkeleton
- - id: DragonSpawn
- 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: ParadoxCloneSpawn
- - id: RevenantSpawn
- id: SleeperAgents
- id: ZombieOutbreak
- id: LoneOpsSpawn
- - id: DerelictCyborgSpawn
- id: WizardSpawn
- type: entity
pickPlayer: false
mindRoles:
- MindRoleDragon
+ - type: DynamicRuleCost
+ cost: 75
- type: entity
parent: BaseGameRule
nameFormat: name-format-ninja
mindRoles:
- MindRoleNinja
+ - type: DynamicRuleCost
+ cost: 75
- type: entity
parent: BaseGameRule
sound: /Audio/Misc/paradox_clone_greeting.ogg
mindRoles:
- MindRoleParadoxClone
+ - type: DynamicRuleCost
+ cost: 50
- type: entity
parent: BaseGameRule
- type: InitialInfected
mindRoles:
- MindRoleInitialInfected
+ - type: DynamicRuleCost
+ cost: 200
- type: entity
parent: BaseNukeopsRule
- Syndicate
mindRoles:
- MindRoleNukeops
+ - type: DynamicRuleCost
+ cost: 75
- type: entity
parent: BaseTraitorRule
- Syndicate
mindRoles:
- MindRoleNukeops
+ - type: DynamicRuleCost
+ cost: 200
- type: entity
abstract: true
maxDifficulty: 5
- type: AntagSelection
agentName: traitor-round-end-agent-name
+ - type: DynamicRuleCost
+ cost: 100
- type: entity
parent: BaseTraitorRule
blacklist:
components:
- AntagImmune
- lateJoinAdditional: true
+ lateJoinAdditional: false
mindRoles:
- MindRoleTraitor
- type: HeadRevolutionary
mindRoles:
- MindRoleHeadRevolutionary
+ - type: DynamicRuleCost
+ cost: 200
- type: entity
id: Sandbox
nameFormat: name-format-wizard
mindRoles:
- MindRoleWizard
+ - type: DynamicRuleCost
+ cost: 150
- type: entity
id: Zombie
- type: InitialInfected
mindRoles:
- 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.
# Put this before antags assigned to station jobs, but after non-job antags (NukeOps/Wiz).
tableId: BasicCalmEventsTable
- !type:NestedSelector
tableId: BasicAntagEventsTable
+ - !type:NestedSelector
+ tableId: ModerateAntagEventsTable
- !type:NestedSelector
tableId: CargoGiftsTable
- !type:NestedSelector
- !type:NestedSelector
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
id: SpaceTrafficControlTable
table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp
scheduledGameRules: !type:NestedSelector
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
id: RampingStationEventScheduler
parent: BaseGameRule
- MindRoleThief
briefing:
sound: "/Audio/Misc/thief_greeting.ogg"
+ - type: DynamicRuleCost
+ cost: 75
# Needs testing
- type: entity
- SpaceTrafficControlFriendlyEventScheduler
- 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
id: Secret
alias: