--- /dev/null
+using Content.Server.Antag.Components;
+using Content.Server.Objectives;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Systems;
+
+namespace Content.Server.Antag;
+
+/// <summary>
+/// Adds fixed objectives to an antag made with <c>AntagObjectivesComponent</c>.
+/// </summary>
+public sealed class AntagObjectivesSystem : EntitySystem
+{
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AntagObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
+ }
+
+ private void OnAntagSelected(Entity<AntagObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
+ {
+ Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
+ return;
+ }
+
+ foreach (var id in ent.Comp.Objectives)
+ {
+ _mind.TryAddObjective(mindId, mind, id);
+ }
+ }
+}
--- /dev/null
+using Content.Server.Antag.Components;
+using Content.Server.Objectives;
+using Content.Shared.Mind;
+using Content.Shared.Objectives.Components;
+using Content.Shared.Objectives.Systems;
+using Robust.Shared.Random;
+
+namespace Content.Server.Antag;
+
+/// <summary>
+/// Adds fixed objectives to an antag made with <c>AntagRandomObjectivesComponent</c>.
+/// </summary>
+public sealed class AntagRandomObjectivesSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+ [Dependency] private readonly ObjectivesSystem _objectives = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AntagRandomObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
+ }
+
+ private void OnAntagSelected(Entity<AntagRandomObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
+ {
+ Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
+ return;
+ }
+
+ var difficulty = 0f;
+ foreach (var set in ent.Comp.Sets)
+ {
+ if (!_random.Prob(set.Prob))
+ continue;
+
+ for (var pick = 0; pick < set.MaxPicks && ent.Comp.MaxDifficulty > difficulty; pick++)
+ {
+ if (_objectives.GetRandomObjective(mindId, mind, set.Groups) is not {} objective)
+ continue;
+
+ _mind.AddObjective(mindId, mind, objective);
+ var adding = Comp<ObjectiveComponent>(objective).Difficulty;
+ difficulty += adding;
+ Log.Debug($"Added objective {ToPrettyString(objective):objective} to {ToPrettyString(args.EntityUid):player} with {adding} difficulty");
+ }
+ }
+ }
+}
}
/// <remarks>
- /// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
+ /// Helper to get just the mind entities and not names.
/// </remarks>
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
{
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
+using Content.Server.Objectives;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
+ SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
+
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
return true;
}
+
+ private void OnObjectivesTextGetInfo(Entity<AntagSelectionComponent> ent, ref ObjectivesTextGetInfoEvent args)
+ {
+ if (ent.Comp.AgentName is not {} name)
+ return;
+
+ args.Minds = ent.Comp.SelectedMinds;
+ args.AgentName = Loc.GetString(name);
+ }
}
/// <summary>
--- /dev/null
+using Content.Server.Antag;
+using Content.Shared.Objectives.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Antag.Components;
+
+/// <summary>
+/// Gives antags selected by this rule a fixed list of objectives.
+/// </summary>
+[RegisterComponent, Access(typeof(AntagObjectivesSystem))]
+public sealed partial class AntagObjectivesComponent : Component
+{
+ /// <summary>
+ /// List of static objectives to give.
+ /// </summary>
+ [DataField(required: true)]
+ public List<EntProtoId<ObjectiveComponent>> Objectives = new();
+}
--- /dev/null
+using Content.Server.Antag;
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Antag.Components;
+
+/// <summary>
+/// Gives antags selected by this rule a random list of objectives.
+/// </summary>
+[RegisterComponent, Access(typeof(AntagRandomObjectivesSystem))]
+public sealed partial class AntagRandomObjectivesComponent : Component
+{
+ /// <summary>
+ /// Each set of objectives to add.
+ /// </summary>
+ [DataField(required: true)]
+ public List<AntagObjectiveSet> Sets = new();
+
+ /// <summary>
+ /// If the total difficulty of the currently given objectives exceeds, no more will be given.
+ /// </summary>
+ [DataField(required: true)]
+ public float MaxDifficulty;
+}
+
+/// <summary>
+/// A set of objectives to try picking.
+/// Difficulty is checked over all sets, but each set has its own probability and pick count.
+/// </summary>
+[DataRecord]
+public record struct AntagObjectiveSet()
+{
+ /// <summary>
+ /// The grouping used by the objective system to pick random objectives.
+ /// First a group is picked from these, then an objective from that group.
+ /// </summary>
+ [DataField(required: true)]
+ public ProtoId<WeightedRandomPrototype> Groups = string.Empty;
+
+ /// <summary>
+ /// Probability of this set being used.
+ /// </summary>
+ [DataField]
+ public float Prob = 1f;
+
+ /// <summary>
+ /// Number of times to try picking objectives from this set.
+ /// Even if there is enough difficulty remaining, no more will be given after this.
+ /// </summary>
+ [DataField]
+ public int MaxPicks = 20;
+}
/// Is not serialized.
/// </summary>
public HashSet<ICommonSession> SelectedSessions = new();
+
+ /// <summary>
+ /// Locale id for the name of the antag.
+ /// If this is set then the antag is listed in the round-end summary.
+ /// </summary>
+ [DataField]
+ public LocId? AgentName;
}
[DataDefinition]
/// Stores data for <see cref="ThiefRuleSystem"/>.
/// </summary>
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
-public sealed partial class ThiefRuleComponent : Component
-{
- [DataField]
- public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups";
-
- [DataField]
- public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups";
-
- [DataField]
- public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
-
- [DataField]
- public float BigObjectiveChance = 0.7f;
-
- [DataField]
- public float MaxObjectiveDifficulty = 2.5f;
-
- [DataField]
- public int MaxStealObjectives = 10;
-}
+public sealed partial class ThiefRuleComponent : Component;
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
- [DataField]
- public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups";
-
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
/// </summary>
[DataField]
public int StartingBalance = 20;
-
- [DataField]
- public int MaxDifficulty = 5;
}
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
+using Content.Shared.Mind;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
namespace Content.Server.GameTicking.Rules;
private void OnObjectivesTextGetInfo(EntityUid uid, GenericAntagRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
- args.Minds = comp.Minds;
+ // just temporary until this is deleted
+ args.Minds = comp.Minds.Select(mindId => (mindId, Comp<MindComponent>(mindId).CharacterName ?? "?")).ToList();
args.AgentName = Loc.GetString(comp.AgentName);
}
}
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
- SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
return;
//Generate objectives
- GenerateObjectives(mindId, mind, ent);
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
}
- private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
- {
- // Give thieves their objectives
- var difficulty = 0f;
-
- if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
- {
- var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup);
- if (objective != null)
- {
- _mindSystem.AddObjective(mindId, mind, objective.Value);
- difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
- }
- }
-
- for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives
- {
- var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup);
- if (objective == null)
- continue;
-
- _mindSystem.AddObjective(mindId, mind, objective.Value);
- difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
- }
-
- //Escape target
- var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup);
- if (escapeObjective != null)
- _mindSystem.AddObjective(mindId, mind, escapeObjective.Value);
- }
-
//Add mind briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
{
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
return briefing;
}
-
- private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
- {
- args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
- args.AgentName = Loc.GetString("thief-round-end-agent-name");
- }
}
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
- public const int MaxPicks = 20;
-
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
- SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
}
}
- public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
+ public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
- // Give traitors their objectives
- if (giveObjectives)
- {
- var difficulty = 0f;
- for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
- {
- var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
- if (objective == null)
- continue;
-
- _mindSystem.AddObjective(mindId, mind, objective.Value);
- var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty;
- difficulty += adding;
- Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty");
- }
- }
-
return true;
}
- private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
- {
- args.Minds = _antag.GetAntagMindEntityUids(uid);
- args.AgentName = Loc.GetString("traitor-round-end-agent-name");
- }
-
+ // TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
+ // TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{
var sb = new StringBuilder();
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
// go through each gamerule getting data for the roundend summary.
- var summaries = new Dictionary<string, Dictionary<string, List<EntityUid>>>();
+ var summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>();
var query = EntityQueryEnumerator<GameRuleComponent>();
while (query.MoveNext(out var uid, out var gameRule))
{
if (!_gameTicker.IsGameRuleAdded(uid, gameRule))
continue;
- var info = new ObjectivesTextGetInfoEvent(new List<EntityUid>(), string.Empty);
+ var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty);
RaiseLocalEvent(uid, ref info);
if (info.Minds.Count == 0)
continue;
// first group the gamerules by their agents, for example 2 different dragons
var agent = info.AgentName;
if (!summaries.ContainsKey(agent))
- summaries[agent] = new Dictionary<string, List<EntityUid>>();
+ summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>();
var prepend = new ObjectivesTextPrependEvent("");
RaiseLocalEvent(uid, ref prepend);
foreach (var (_, minds) in summary)
{
total += minds.Count;
- totalInCustody += minds.Where(m => IsInCustody(m)).Count();
+ totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count();
}
var result = new StringBuilder();
}
}
- private void AddSummary(StringBuilder result, string agent, List<EntityUid> minds)
+ private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
{
var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();
- foreach (var mindId in minds)
+ foreach (var (mindId, name) in minds)
{
- if (!TryComp(mindId, out MindComponent? mind))
- continue;
-
- var title = GetTitle(mindId, mind);
- if (title == null)
+ if (!TryComp<MindComponent>(mindId, out var mind))
continue;
+ var title = GetTitle((mindId, mind), name);
var custody = IsInCustody(mindId, mind) ? Loc.GetString("objectives-in-custody") : string.Empty;
var objectives = mind.Objectives;
/// <summary>
/// Get the title for a player's mind used in round end.
+ /// Pass in the original entity name which is shown alongside username.
/// </summary>
- public string? GetTitle(EntityUid mindId, MindComponent? mind = null)
+ public string GetTitle(Entity<MindComponent?> mind, string name)
{
- if (!Resolve(mindId, ref mind))
- return null;
-
- var name = mind.CharacterName;
- var username = (string?) null;
-
- if (mind.OriginalOwnerUserId != null &&
- _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
+ if (Resolve(mind, ref mind.Comp) &&
+ mind.Comp.OriginalOwnerUserId != null &&
+ _player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData))
{
- username = sessionData.UserName;
+ var username = sessionData.UserName;
+ return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
}
-
- if (username != null)
- {
- if (name != null)
- return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
-
- return Loc.GetString("objectives-player-user", ("user", username));
- }
-
- // nothing to identify the player by, just give up
- if (name == null)
- return null;
-
return Loc.GetString("objectives-player-named", ("name", name));
}
}
/// The objectives system already checks if the game rule is added so you don't need to check that in this event's handler.
/// </remarks>
[ByRefEvent]
-public record struct ObjectivesTextGetInfoEvent(List<EntityUid> Minds, string AgentName);
+public record struct ObjectivesTextGetInfoEvent(List<(EntityUid, string)> Minds, string AgentName);
/// <summary>
/// Raised on the game rule before text for each agent's objectives is added, letting you prepend something.
objectives-round-end-result-in-custody = {$custody} out of {$count} {MAKEPLURAL($agent)} were in custody.
objectives-player-user-named = [color=White]{$name}[/color] ([color=gray]{$user}[/color])
-objectives-player-user = [color=gray]{$user}[/color]
objectives-player-named = [color=White]{$name}[/color]
objectives-no-objectives = {$custody}{$title} was a {$agent}.
prototype: Nukeops
- type: entity
- id: SleeperAgentsRule
- parent: BaseGameRule
noSpawn: true
+ parent: BaseTraitorRule
+ id: SleeperAgentsRule
components:
- type: StationEvent
earliestStart: 30
startAudio:
path: /Audio/Announcements/intercept.ogg
- type: AlertLevelInterceptionRule
- - type: TraitorRule
- type: AntagSelection
definitions:
- prefRoles: [ Traitor ]
id: Thief
components:
- type: ThiefRule
+ - type: AntagObjectives
+ objectives:
+ - EscapeThiefShuttleObjective
+ - type: AntagRandomObjectives
+ sets:
+ - groups: ThiefBigObjectiveGroups
+ prob: 0.7
+ maxPicks: 1
+ - groups: ThiefObjectiveGroups
+ maxPicks: 10
+ maxDifficulty: 2.5
- type: AntagSelection
+ agentName: thief-round-end-agent-name
definitions:
- prefRoles: [ Thief ]
maxRange:
prototype: Thief
briefing:
sound: "/Audio/Misc/thief_greeting.ogg"
-
-#- type: entity
-# noSpawn: true
-# parent: BaseGameRule
-# id: Exterminator
-# components:
-# - type: GenericAntagRule
-# agentName: terminator-round-end-agent-name
-# objectives:
-# - TerminateObjective
-# - ShutDownObjective
prototype: Nukeops
- type: entity
- id: Traitor
+ abstract: true
parent: BaseGameRule
+ id: BaseTraitorRule
+ components:
+ - type: TraitorRule
+ # TODO: codewords in yml
+ # TODO: uplink in yml
+ - type: AntagRandomObjectives
+ sets:
+ - groups: TraitorObjectiveGroups
+ maxDifficulty: 5
+ - type: AntagSelection
+ agentName: traitor-round-end-agent-name
+
+- type: entity
noSpawn: true
+ parent: BaseTraitorRule
+ id: Traitor
components:
- type: GameRule
minPlayers: 5
delay:
min: 240
max: 420
- - type: TraitorRule
- type: AntagSelection
definitions:
- prefRoles: [ Traitor ]
ThiefObjectiveGroupStructure: 0 #Temporarily disabled until obvious ways to steal structures are added
ThiefObjectiveGroupAnimal: 2
-- type: weightedRandom
- id: ThiefEscapeObjectiveGroups
- weights:
- ThiefObjectiveGroupEscape: 1
-
-
-
- type: weightedRandom
id: ThiefObjectiveGroupCollection
weights: